Skip to main content

made_core/entities/ceremony_instance/
interventions.rs

1use super::{
2    CeremonyDefinition, CeremonyEvidencePack, CeremonyEvidenceRequest, CeremonyEvidenceSourceId,
3    CeremonyGuardApproval, CeremonyGuardDeferral, CeremonyInstance, CeremonyIntervention,
4    CeremonyInterventionContent, CeremonyInterventionId, CeremonyInterventionKind,
5    CeremonyInterventionProvenance, CeremonyInterventionResponse, CeremonyInterventionTarget,
6    CeremonyReason, CeremonyReasonKind, CeremonyRecordRef, CeremonyTransitionRecord, DomainError,
7    MemoryConfidence, OffsetDateTime, ReasonAsserter, RoleAction, RoleId,
8};
9
10impl CeremonyInstance {
11    #[allow(clippy::too_many_arguments)]
12    pub fn request_intervention_as(
13        &mut self,
14        definition: &CeremonyDefinition,
15        intervention_id: CeremonyInterventionId,
16        role_id: RoleId,
17        kind: CeremonyInterventionKind,
18        target: CeremonyInterventionTarget,
19        content: CeremonyInterventionContent,
20        now: OffsetDateTime,
21    ) -> Result<(), DomainError> {
22        self.request_intervention_with_provenance_as(
23            definition,
24            intervention_id,
25            role_id,
26            kind,
27            target,
28            content,
29            None,
30            now,
31        )
32    }
33
34    #[allow(clippy::too_many_arguments)]
35    pub fn request_intervention_with_provenance_as(
36        &mut self,
37        definition: &CeremonyDefinition,
38        intervention_id: CeremonyInterventionId,
39        role_id: RoleId,
40        kind: CeremonyInterventionKind,
41        target: CeremonyInterventionTarget,
42        content: CeremonyInterventionContent,
43        provenance: Option<CeremonyInterventionProvenance>,
44        now: OffsetDateTime,
45    ) -> Result<(), DomainError> {
46        self.require_active(
47            definition,
48            "terminal ceremony instances cannot accept interventions",
49        )?;
50        self.require_role(definition, &role_id, &RoleAction::request_intervention())?;
51        Self::require_intervention_target(definition, &target)?;
52        if let Some(provenance) = provenance.as_ref() {
53            self.require_intervention_provenance(definition, &role_id, &target, provenance)?;
54        }
55        if self
56            .interventions
57            .iter()
58            .any(|intervention| intervention.id() == &intervention_id)
59        {
60            return Err(DomainError::AlreadyExists {
61                what: "ceremony_intervention",
62            });
63        }
64        let intervention = CeremonyIntervention::open_with_provenance(
65            intervention_id,
66            kind,
67            role_id,
68            target,
69            content,
70            provenance,
71            now,
72        );
73        self.interventions.push(intervention);
74        self.updated_at = now;
75        Ok(())
76    }
77
78    pub fn respond_to_intervention_as(
79        &mut self,
80        definition: &CeremonyDefinition,
81        intervention_id: &CeremonyInterventionId,
82        role_id: RoleId,
83        content: CeremonyInterventionContent,
84        now: OffsetDateTime,
85    ) -> Result<(), DomainError> {
86        self.require_active(
87            definition,
88            "terminal ceremony instances cannot receive intervention responses",
89        )?;
90        self.require_role(definition, &role_id, &RoleAction::respond_to_intervention())?;
91        self.interventions
92            .iter_mut()
93            .find(|intervention| intervention.id() == intervention_id)
94            .ok_or(DomainError::NotFound {
95                what: "ceremony_intervention",
96            })?
97            .respond(role_id, content, now)?;
98        self.record_that_it_answers(intervention_id, now);
99        self.updated_at = now;
100        Ok(())
101    }
102
103    pub fn prepare_evidence_request_as(
104        &self,
105        definition: &CeremonyDefinition,
106        intervention_id: CeremonyInterventionId,
107        role_id: RoleId,
108        source_id: CeremonyEvidenceSourceId,
109        query: CeremonyInterventionContent,
110    ) -> Result<CeremonyEvidenceRequest, DomainError> {
111        self.require_active(
112            definition,
113            "terminal ceremony instances cannot collect intervention evidence",
114        )?;
115        self.require_role(definition, &role_id, &RoleAction::respond_to_intervention())?;
116        self.intervention(&intervention_id)
117            .ok_or(DomainError::NotFound {
118                what: "ceremony_intervention",
119            })?
120            .ensure_can_respond(&role_id)?;
121        Ok(CeremonyEvidenceRequest::new(
122            self.id.clone(),
123            intervention_id,
124            role_id,
125            source_id,
126            query,
127            self.context.clone(),
128        ))
129    }
130
131    pub fn respond_to_intervention_with_evidence_as(
132        &mut self,
133        definition: &CeremonyDefinition,
134        intervention_id: &CeremonyInterventionId,
135        role_id: RoleId,
136        evidence_pack: CeremonyEvidencePack,
137        now: OffsetDateTime,
138    ) -> Result<(), DomainError> {
139        self.require_active(
140            definition,
141            "terminal ceremony instances cannot receive intervention evidence",
142        )?;
143        self.require_role(definition, &role_id, &RoleAction::respond_to_intervention())?;
144        self.interventions
145            .iter_mut()
146            .find(|intervention| intervention.id() == intervention_id)
147            .ok_or(DomainError::NotFound {
148                what: "ceremony_intervention",
149            })?
150            .respond_with_evidence(role_id, evidence_pack, now)?;
151        self.record_that_it_answers(intervention_id, now);
152        self.updated_at = now;
153        Ok(())
154    }
155
156    /// State why one thing here led to another.
157    ///
158    /// Its own act rather than a field on contributing, because a
159    /// reason is often known later — "in fact I did that because…" is
160    /// how people reason — and because a field gets filled in by
161    /// inertia while an act is chosen.
162    ///
163    /// What it refuses is the point:
164    ///
165    /// - a kind only the engine may assert, because a participant able
166    ///   to relabel the structure could rewrite the session's shape;
167    /// - a kind only an author may assert, claimed by anyone else,
168    ///   because nobody else has access to another's reasoning;
169    /// - either end naming something this session never produced.
170    #[allow(clippy::too_many_arguments)]
171    pub fn assert_reason_as(
172        &mut self,
173        definition: &CeremonyDefinition,
174        role_id: RoleId,
175        from: CeremonyRecordRef,
176        to: CeremonyRecordRef,
177        kind: CeremonyReasonKind,
178        why: impl Into<String>,
179        confidence: MemoryConfidence,
180        now: OffsetDateTime,
181    ) -> Result<(), DomainError> {
182        self.require_declared_role(definition, &role_id)?;
183        self.require_record(&from)?;
184        self.require_record(&to)?;
185
186        match kind.asserter() {
187            ReasonAsserter::TheEngine => {
188                return Err(DomainError::InvariantViolated {
189                    reason:
190                        "this kind of reason states the shape of the session, not a judgement, \
191                             and only the engine may assert it",
192                });
193            }
194            ReasonAsserter::ItsAuthor => {
195                if self.author_of(&from) != Some(&role_id) {
196                    return Err(DomainError::InvariantViolated {
197                        reason: "only whoever produced something may say why they decided it or \
198                                 how they did it",
199                    });
200                }
201            }
202            ReasonAsserter::AnySeat => {}
203        }
204
205        self.reasons.push(CeremonyReason::new(
206            from,
207            to,
208            kind,
209            why,
210            confidence,
211            Some(role_id),
212            now,
213        )?);
214        self.updated_at = now;
215        Ok(())
216    }
217
218    /// Who produced a record, where anyone did.
219    ///
220    /// A step has none: the engine ran it. A transition the engine
221    /// took has none either. Both are absences rather than gaps, and
222    /// they are what stops a reason of testimony being made about
223    /// something nobody can testify to.
224    fn author_of(&self, record: &CeremonyRecordRef) -> Option<&RoleId> {
225        match record {
226            CeremonyRecordRef::Step { .. } => None,
227            CeremonyRecordRef::AgendaItem { agenda_item } => self
228                .intervention(agenda_item)
229                .map(CeremonyIntervention::requested_by),
230            CeremonyRecordRef::Contribution {
231                agenda_item,
232                ordinal,
233            } => self
234                .intervention(agenda_item)
235                .and_then(|item| item.responses().get(*ordinal as usize))
236                .map(CeremonyInterventionResponse::role_id),
237            CeremonyRecordRef::GuardDecision { guard_name } => self
238                .guard_approvals
239                .iter()
240                .find(|approval| approval.guard_name() == guard_name)
241                .map(CeremonyGuardApproval::approved_by)
242                .or_else(|| {
243                    self.guard_deferrals
244                        .iter()
245                        .find(|deferral| deferral.guard_name() == guard_name)
246                        .map(CeremonyGuardDeferral::deferred_by)
247                }),
248            CeremonyRecordRef::Transition { ordinal } => self
249                .transitions
250                .get(ordinal.saturating_sub(1) as usize)
251                .and_then(CeremonyTransitionRecord::applied_by),
252        }
253    }
254
255    /// A record this session actually produced.
256    ///
257    /// Memory cannot check this — an edge there may reach something
258    /// written an hour ago — but a session knows everything it has
259    /// done, and letting a reason cite what never happened would be
260    /// declining to use the one advantage it has.
261    fn require_record(&self, record: &CeremonyRecordRef) -> Result<(), DomainError> {
262        let exists = match record {
263            CeremonyRecordRef::Step { step_id } => self.step_records.contains_key(step_id),
264            CeremonyRecordRef::AgendaItem { agenda_item } => {
265                self.intervention(agenda_item).is_some()
266            }
267            CeremonyRecordRef::Contribution {
268                agenda_item,
269                ordinal,
270            } => self
271                .intervention(agenda_item)
272                .is_some_and(|item| item.responses().len() > *ordinal as usize),
273            CeremonyRecordRef::GuardDecision { guard_name } => {
274                self.guard_approvals
275                    .iter()
276                    .any(|approval| approval.guard_name() == guard_name)
277                    || self
278                        .guard_deferrals
279                        .iter()
280                        .any(|deferral| deferral.guard_name() == guard_name)
281            }
282            CeremonyRecordRef::Transition { ordinal } => {
283                *ordinal >= 1 && (*ordinal as usize) <= self.transitions.len()
284            }
285        };
286        if exists {
287            Ok(())
288        } else {
289            Err(DomainError::NotFound {
290                what: "ceremony_record",
291            })
292        }
293    }
294
295    /// The reason the engine can see on its own: a contribution is the
296    /// reply to the item it was made against.
297    ///
298    /// The only kind it asserts. Everything explanatory comes from
299    /// whoever reasoned, because a session ending well after an action
300    /// is not the action having worked.
301    fn record_that_it_answers(
302        &mut self,
303        agenda_item: &CeremonyInterventionId,
304        now: OffsetDateTime,
305    ) {
306        let Some(ordinal) = self
307            .intervention(agenda_item)
308            .map(|item| item.responses().len())
309            .and_then(|count| u32::try_from(count.checked_sub(1)?).ok())
310        else {
311            return;
312        };
313        if let Ok(reason) = CeremonyReason::new(
314            CeremonyRecordRef::contribution(agenda_item.clone(), ordinal),
315            CeremonyRecordRef::agenda_item(agenda_item.clone()),
316            CeremonyReasonKind::Answers,
317            "a contribution made against this agenda item",
318            MemoryConfidence::High,
319            None,
320            now,
321        ) {
322            self.reasons.push(reason);
323        }
324    }
325
326    pub fn close_intervention_as(
327        &mut self,
328        definition: &CeremonyDefinition,
329        intervention_id: &CeremonyInterventionId,
330        role_id: &RoleId,
331        now: OffsetDateTime,
332    ) -> Result<(), DomainError> {
333        self.require_active(
334            definition,
335            "terminal ceremony instances cannot close interventions",
336        )?;
337        self.require_role(definition, role_id, &RoleAction::request_intervention())?;
338        self.interventions
339            .iter_mut()
340            .find(|intervention| intervention.id() == intervention_id)
341            .ok_or(DomainError::NotFound {
342                what: "ceremony_intervention",
343            })?
344            .close(role_id, now)?;
345        self.updated_at = now;
346        Ok(())
347    }
348}