Skip to main content

bacnet_server/event_enrollment/
commit.rs

1use std::collections::{HashMap, HashSet};
2
3use bacnet_objects::database::ObjectDatabase;
4use bacnet_objects::event::{
5    EventStateChange, EventTransition, EventTransitionCommit, EventTransitionCommitError,
6};
7use bacnet_objects::event_enrollment::EventEnrollmentReliabilityCommit;
8use bacnet_objects::event_enrollment::{EventEnrollmentEvalState, EventEnrollmentMonitoredSource};
9use bacnet_types::enums::{EventState, EventType, PropertyIdentifier, Reliability};
10use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
11
12use super::EventEnrollmentTransition;
13use crate::server::event_notification_payload::{
14    project_event_enrollment_payload, CapturedReferencedValue, CommittedNotificationPayload,
15    EventEnrollmentProjectionSnapshot,
16};
17use crate::server::event_timestamp::{
18    confirm_event_timestamp, stage_event_timestamp, SampledEventClock,
19};
20
21/// Stage at which an Event Enrollment evaluation result was committed.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum EventEnrollmentEvaluationStage {
24    /// The enrollment produced no mutation or transition proposal.
25    Evaluation,
26    /// Monitored-source ownership was being updated.
27    EvaluationSource,
28    /// Private countdown or baseline state was being updated.
29    EvaluationState,
30    /// The atomic Event_State/Acked_Transitions/Event_Time_Stamps hook ran.
31    EventTransition,
32}
33
34/// Observable result of one Event Enrollment commit stage.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum EventEnrollmentEvaluationOutcome {
37    /// Evaluation completed without a transition.
38    NoTransition,
39    /// A pending transition was canceled and its private state was stored.
40    CancellationCommitted,
41    /// A required internal mutation was rejected.
42    Rejected,
43    /// A custom hook returned an error after the target Event_State landed.
44    ///
45    /// This violates the atomic hook contract. The evaluator suppresses the
46    /// transition token, does not consume the staged clockless sequence
47    /// number, and invalidates private evaluation state for a later reset.
48    LandedAfterError,
49}
50
51impl EventEnrollmentEvaluationOutcome {
52    /// Whether this outcome represents a commit failure.
53    pub fn is_failure(self) -> bool {
54        matches!(self, Self::Rejected | Self::LandedAfterError)
55    }
56}
57
58/// One structured Event Enrollment evaluation diagnostic.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub struct EventEnrollmentEvaluationDiagnostic {
61    /// Enrollment whose evaluation produced this diagnostic.
62    pub enrollment_oid: ObjectIdentifier,
63    /// Commit stage that produced the outcome.
64    pub stage: EventEnrollmentEvaluationStage,
65    /// Observable stage outcome.
66    pub outcome: EventEnrollmentEvaluationOutcome,
67}
68
69/// Stage exposed by the additive detailed Event Enrollment evaluator.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum EventEnrollmentDetailedEvaluationStage {
72    /// The enrollment produced no mutation or transition proposal.
73    Evaluation,
74    /// Monitored-source ownership was being updated.
75    EvaluationSource,
76    /// Private countdown or baseline state was being updated.
77    EvaluationState,
78    /// Reliability observation or the combined Reliability transition hook ran.
79    Reliability,
80    /// The atomic Event_State/Acked_Transitions/Event_Time_Stamps hook ran.
81    EventTransition,
82}
83
84/// Outcome exposed by the additive detailed Event Enrollment evaluator.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum EventEnrollmentDetailedEvaluationOutcome {
87    /// Evaluation completed without a transition.
88    NoTransition,
89    /// A pending transition was canceled and its private state was stored.
90    CancellationCommitted,
91    /// A required local or remote observation was temporarily unavailable.
92    ///
93    /// No public Reliability, Event_State, acknowledgement, event-history,
94    /// timestamp, or sequence transition is committed. The repository-local
95    /// D4 policy may clear private continuity state and monitored-source
96    /// ownership.
97    ObservationUnavailable,
98    /// A required internal mutation was rejected.
99    Rejected,
100    /// A custom hook returned an error after Reliability or Event_State landed.
101    ///
102    /// This violates the atomic hook contract. The evaluator suppresses the
103    /// result token, does not consume the staged clockless sequence number,
104    /// and invalidates private evaluation state for a later reset.
105    LandedAfterError,
106}
107
108impl EventEnrollmentDetailedEvaluationOutcome {
109    /// Whether this outcome represents a commit failure.
110    pub fn is_failure(self) -> bool {
111        matches!(self, Self::Rejected | Self::LandedAfterError)
112    }
113}
114
115/// One diagnostic from the additive detailed Event Enrollment evaluator.
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub struct EventEnrollmentDetailedEvaluationDiagnostic {
118    /// Enrollment whose evaluation produced this diagnostic.
119    pub enrollment_oid: ObjectIdentifier,
120    /// Evaluation or commit stage that produced the outcome.
121    pub stage: EventEnrollmentDetailedEvaluationStage,
122    /// Observable stage outcome.
123    pub outcome: EventEnrollmentDetailedEvaluationOutcome,
124}
125
126/// Precedence source that selected a committed Event Enrollment Reliability.
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub enum EventEnrollmentReliabilityCause {
129    /// A definitive local configuration defect selected CONFIGURATION_ERROR.
130    Configuration,
131    /// The monitored object's nonzero Reliability selected MONITORED_OBJECT_FAULT.
132    MonitoredObject,
133    /// A supported configured fault algorithm selected the value or recovery.
134    FaultAlgorithm,
135}
136
137/// One successfully committed Event Enrollment Reliability result.
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct EventEnrollmentReliabilityResult {
140    /// Event Enrollment object whose atomic commit succeeded.
141    pub enrollment_oid: ObjectIdentifier,
142    /// Configured monitored object, absent for a malformed/missing reference.
143    pub monitored_oid: Option<ObjectIdentifier>,
144    /// Reliability value read before the commit.
145    pub previous_reliability: Reliability,
146    /// Reliability value stored by the commit.
147    pub new_reliability: Reliability,
148    /// FAULT entry, re-entry, or recovery committed with Reliability.
149    pub state_change: Option<EventStateChange>,
150    /// Whether Event_Enable permits distribution for the transition coordinate.
151    pub distribute: bool,
152    /// Precedence source that selected `new_reliability`.
153    pub cause: EventEnrollmentReliabilityCause,
154}
155
156impl EventEnrollmentReliabilityResult {
157    /// Select the event type for this committed Reliability result.
158    ///
159    /// `configured_algorithm` is the Event Enrollment object's configured
160    /// Event_Type. `None` means that the result has no committed state change.
161    pub fn event_type(&self, configured_algorithm: EventType) -> Option<EventType> {
162        self.state_change
163            .as_ref()
164            .map(|change| change.event_type(configured_algorithm))
165    }
166}
167
168/// Legacy result of one Event Enrollment evaluation pass.
169#[derive(Debug, Clone, Default, PartialEq)]
170pub struct EventEnrollmentEvaluationReport {
171    /// Transitions whose complete atomic object commit succeeded.
172    pub transitions: Vec<EventEnrollmentTransition>,
173    /// Non-transition and failure diagnostics in enrollment evaluation order.
174    pub diagnostics: Vec<EventEnrollmentEvaluationDiagnostic>,
175}
176
177/// Additive detailed result of one Event Enrollment evaluation pass.
178#[derive(Debug, Clone, Default, PartialEq)]
179pub struct EventEnrollmentDetailedEvaluationReport {
180    /// Transitions whose complete atomic object commit succeeded.
181    pub transitions: Vec<EventEnrollmentTransition>,
182    /// Reliability results whose complete combined object commit succeeded.
183    pub reliability_results: Vec<EventEnrollmentReliabilityResult>,
184    /// Every detailed evaluation, observation, and commit diagnostic.
185    pub diagnostics: Vec<EventEnrollmentDetailedEvaluationDiagnostic>,
186}
187
188/// Public result family carried by one private, already-committed delivery.
189pub(crate) enum CommittedEventEnrollmentResult {
190    Normal(EventEnrollmentTransition),
191    Reliability(EventEnrollmentReliabilityResult),
192}
193
194/// One Event Enrollment commit plus policy captured under its database guard.
195pub(crate) struct CommittedEventEnrollmentDelivery {
196    pub(crate) result: CommittedEventEnrollmentResult,
197    pub(crate) ack_required: bool,
198    pub(crate) recipient_clock: SampledEventClock,
199    pub(crate) event_type: EventType,
200    pub(crate) event_values: CommittedNotificationPayload,
201}
202
203/// Public report plus the commit-order stream consumed only by the server.
204pub(crate) struct EventEnrollmentEvaluationBatch {
205    pub(crate) report: EventEnrollmentDetailedEvaluationReport,
206    pub(crate) deliveries: Vec<CommittedEventEnrollmentDelivery>,
207}
208
209impl EventEnrollmentDetailedEvaluationReport {
210    pub(super) fn into_legacy(self) -> EventEnrollmentEvaluationReport {
211        let diagnostics = self
212            .diagnostics
213            .into_iter()
214            .map(|diagnostic| {
215                let stage = match (diagnostic.stage, diagnostic.outcome) {
216                    (
217                        EventEnrollmentDetailedEvaluationStage::Reliability,
218                        EventEnrollmentDetailedEvaluationOutcome::ObservationUnavailable
219                        | EventEnrollmentDetailedEvaluationOutcome::NoTransition,
220                    ) => EventEnrollmentEvaluationStage::Evaluation,
221                    (EventEnrollmentDetailedEvaluationStage::Reliability, _) => {
222                        EventEnrollmentEvaluationStage::EventTransition
223                    }
224                    (EventEnrollmentDetailedEvaluationStage::Evaluation, _) => {
225                        EventEnrollmentEvaluationStage::Evaluation
226                    }
227                    (EventEnrollmentDetailedEvaluationStage::EvaluationSource, _) => {
228                        EventEnrollmentEvaluationStage::EvaluationSource
229                    }
230                    (EventEnrollmentDetailedEvaluationStage::EvaluationState, _) => {
231                        EventEnrollmentEvaluationStage::EvaluationState
232                    }
233                    (EventEnrollmentDetailedEvaluationStage::EventTransition, _) => {
234                        EventEnrollmentEvaluationStage::EventTransition
235                    }
236                };
237                let outcome = match diagnostic.outcome {
238                    EventEnrollmentDetailedEvaluationOutcome::ObservationUnavailable => {
239                        EventEnrollmentEvaluationOutcome::NoTransition
240                    }
241                    EventEnrollmentDetailedEvaluationOutcome::NoTransition => {
242                        EventEnrollmentEvaluationOutcome::NoTransition
243                    }
244                    EventEnrollmentDetailedEvaluationOutcome::CancellationCommitted => {
245                        EventEnrollmentEvaluationOutcome::CancellationCommitted
246                    }
247                    EventEnrollmentDetailedEvaluationOutcome::Rejected => {
248                        EventEnrollmentEvaluationOutcome::Rejected
249                    }
250                    EventEnrollmentDetailedEvaluationOutcome::LandedAfterError => {
251                        EventEnrollmentEvaluationOutcome::LandedAfterError
252                    }
253                };
254                EventEnrollmentEvaluationDiagnostic {
255                    enrollment_oid: diagnostic.enrollment_oid,
256                    stage,
257                    outcome,
258                }
259            })
260            .collect();
261        EventEnrollmentEvaluationReport {
262            transitions: self.transitions,
263            diagnostics,
264        }
265    }
266}
267
268pub(crate) fn log_evaluation_report(report: &EventEnrollmentDetailedEvaluationReport) {
269    for result in &report.reliability_results {
270        tracing::debug!(
271            enrollment = %result.enrollment_oid,
272            monitored = ?result.monitored_oid,
273            previous = ?result.previous_reliability,
274            new = ?result.new_reliability,
275            state_change = ?result.state_change,
276            distribute = result.distribute,
277            cause = ?result.cause,
278            "Event enrollment: reliability committed"
279        );
280    }
281    for diagnostic in report
282        .diagnostics
283        .iter()
284        .filter(|diagnostic| diagnostic.outcome.is_failure())
285    {
286        tracing::warn!(
287            enrollment = %diagnostic.enrollment_oid,
288            stage = ?diagnostic.stage,
289            outcome = ?diagnostic.outcome,
290            "Event enrollment: internal commit failed"
291        );
292    }
293}
294
295/// Coalesced desired mutation for one enrollment and one evaluation pass.
296#[derive(Default)]
297pub(super) struct EnrollmentUpdate {
298    eval_state: Option<EventEnrollmentEvalState>,
299    eval_source: Option<Option<EventEnrollmentMonitoredSource>>,
300    clears_invalidation: bool,
301    fired: Option<FiredTransition>,
302    reliability: Option<ReliabilityUpdate>,
303    observation_unavailable: bool,
304    canceled: bool,
305}
306
307pub(super) struct FiredTransition {
308    pub(super) monitored_oid: ObjectIdentifier,
309    pub(super) event_type_raw: u32,
310    pub(super) from: EventState,
311    pub(super) to: EventState,
312    pub(super) distribute: bool,
313    pub(super) ack_required: bool,
314    pub(super) projection: EventEnrollmentProjectionSnapshot,
315}
316
317pub(super) struct ReliabilityUpdate {
318    pub(super) monitored_oid: Option<ObjectIdentifier>,
319    pub(super) previous: Reliability,
320    pub(super) desired: Reliability,
321    pub(super) from: EventState,
322    pub(super) to: EventState,
323    pub(super) distribute: bool,
324    pub(super) ack_required: bool,
325    pub(super) cause: EventEnrollmentReliabilityCause,
326    pub(super) referenced_value: CapturedReferencedValue,
327}
328
329impl EnrollmentUpdate {
330    /// Replace the desired private state with the state reached later in the
331    /// same deterministic phase-1 flow. This is semantic coalescing: reset,
332    /// cancellation, and reseed operations all update one final snapshot.
333    pub(super) fn set_eval_state(&mut self, state: EventEnrollmentEvalState) {
334        self.eval_state = Some(state);
335    }
336
337    pub(super) fn reset_eval_state(&mut self) {
338        self.eval_state = Some(EventEnrollmentEvalState::default());
339        self.clears_invalidation = true;
340    }
341
342    pub(super) fn cancel_pending(&mut self, state: EventEnrollmentEvalState) {
343        self.eval_state = Some(state);
344        self.canceled = true;
345    }
346
347    pub(super) fn set_eval_source(&mut self, source: Option<EventEnrollmentMonitoredSource>) {
348        self.eval_source = Some(source);
349    }
350
351    /// Replace every source mutation staged earlier in this pass with the
352    /// minimal owner-clear required by the pre-pass source coordinate.
353    pub(super) fn reset_eval_source_for_observation_gap(
354        &mut self,
355        previous: Option<Option<EventEnrollmentMonitoredSource>>,
356    ) {
357        self.eval_source = match previous {
358            Some(Some(_)) => Some(None),
359            Some(None) | None => None,
360        };
361    }
362
363    pub(super) fn fire(&mut self, fired: FiredTransition) {
364        debug_assert!(
365            self.fired.is_none(),
366            "one enrollment fires at most once per pass"
367        );
368        self.fired = Some(fired);
369    }
370
371    pub(super) fn set_reliability(&mut self, reliability: ReliabilityUpdate) {
372        debug_assert!(
373            self.reliability.is_none() && self.fired.is_none(),
374            "one enrollment commits one transition family per pass"
375        );
376        self.reliability = Some(reliability);
377    }
378
379    pub(super) fn observation_unavailable(&mut self) {
380        // A terminal gap cannot leak a transition staged earlier in the same
381        // deterministic phase-1 flow.
382        self.fired = None;
383        self.reliability = None;
384        self.canceled = false;
385        self.observation_unavailable = true;
386    }
387}
388
389fn diagnostic(
390    enrollment_oid: ObjectIdentifier,
391    stage: EventEnrollmentDetailedEvaluationStage,
392    outcome: EventEnrollmentDetailedEvaluationOutcome,
393) -> EventEnrollmentDetailedEvaluationDiagnostic {
394    EventEnrollmentDetailedEvaluationDiagnostic {
395        enrollment_oid,
396        stage,
397        outcome,
398    }
399}
400
401fn event_state_landed(db: &ObjectDatabase, oid: &ObjectIdentifier, state: EventState) -> bool {
402    matches!(
403        db.get(oid)
404            .and_then(|object| object.read_property(PropertyIdentifier::EVENT_STATE, None).ok()),
405        Some(PropertyValue::Enumerated(raw)) if raw == state.to_raw()
406    )
407}
408
409fn reliability_or_state_landed(
410    db: &ObjectDatabase,
411    oid: &ObjectIdentifier,
412    update: &ReliabilityUpdate,
413) -> bool {
414    let Some(object) = db.get(oid) else {
415        return false;
416    };
417    let reliability_landed = update.previous != update.desired
418        && matches!(
419            object.read_property(PropertyIdentifier::RELIABILITY, None),
420            Ok(PropertyValue::Enumerated(raw)) if raw == update.desired.to_raw()
421        );
422    let state_landed = update.from != update.to
423        && matches!(
424            object.read_property(PropertyIdentifier::EVENT_STATE, None),
425            Ok(PropertyValue::Enumerated(raw)) if raw == update.to.to_raw()
426        );
427    reliability_landed || state_landed
428}
429
430fn clear_source_ownership(
431    db: &mut ObjectDatabase,
432    oid: ObjectIdentifier,
433    database_eval_sources: &HashSet<ObjectIdentifier>,
434) {
435    if database_eval_sources.contains(&oid) {
436        db.set_enrollment_eval_source(oid, None);
437    } else if let Some(object) = db.get_mut(&oid) {
438        let _ = object.set_enrollment_eval_source_internal(None);
439    }
440}
441
442#[cfg(test)]
443pub(super) fn apply_updates(
444    db: &mut ObjectDatabase,
445    oids: &[ObjectIdentifier],
446    updates: HashMap<ObjectIdentifier, EnrollmentUpdate>,
447    database_eval_sources: &HashSet<ObjectIdentifier>,
448) -> EventEnrollmentDetailedEvaluationReport {
449    apply_updates_for_delivery(db, oids, updates, database_eval_sources).report
450}
451
452pub(super) fn apply_updates_for_delivery(
453    db: &mut ObjectDatabase,
454    oids: &[ObjectIdentifier],
455    mut updates: HashMap<ObjectIdentifier, EnrollmentUpdate>,
456    database_eval_sources: &HashSet<ObjectIdentifier>,
457) -> EventEnrollmentEvaluationBatch {
458    let mut report = EventEnrollmentDetailedEvaluationReport::default();
459    let mut deliveries = Vec::new();
460
461    for &oid in oids {
462        let Some(update) = updates.remove(&oid) else {
463            report.diagnostics.push(diagnostic(
464                oid,
465                EventEnrollmentDetailedEvaluationStage::Evaluation,
466                EventEnrollmentDetailedEvaluationOutcome::NoTransition,
467            ));
468            continue;
469        };
470
471        let mut source_failed = false;
472        let mut state_failed = false;
473        if let Some(source) = update.eval_source {
474            if database_eval_sources.contains(&oid) {
475                db.set_enrollment_eval_source(oid, source);
476            } else {
477                let failed = db.get_mut(&oid).is_none_or(|object| {
478                    object.set_enrollment_eval_source_internal(source).is_err()
479                });
480                if failed {
481                    source_failed = true;
482                    report.diagnostics.push(diagnostic(
483                        oid,
484                        EventEnrollmentDetailedEvaluationStage::EvaluationSource,
485                        EventEnrollmentDetailedEvaluationOutcome::Rejected,
486                    ));
487                }
488            }
489        }
490
491        let mut state_committed = false;
492        if source_failed {
493            // Dependent state cannot retain an owner that failed to land.
494            // This is the single allowed private-state setter call this pass.
495            state_failed = db.get_mut(&oid).is_none_or(|object| {
496                object
497                    .set_enrollment_eval_state_internal(EventEnrollmentEvalState::default())
498                    .is_err()
499            });
500            if state_failed {
501                db.set_enrollment_eval_state_invalidated(oid, true);
502                report.diagnostics.push(diagnostic(
503                    oid,
504                    EventEnrollmentDetailedEvaluationStage::EvaluationState,
505                    EventEnrollmentDetailedEvaluationOutcome::Rejected,
506                ));
507            } else {
508                state_committed = true;
509            }
510        } else if let Some(state) = update.eval_state {
511            state_failed = db
512                .get_mut(&oid)
513                .is_none_or(|object| object.set_enrollment_eval_state_internal(state).is_err());
514            if state_failed {
515                clear_source_ownership(db, oid, database_eval_sources);
516                db.set_enrollment_eval_state_invalidated(oid, true);
517                report.diagnostics.push(diagnostic(
518                    oid,
519                    EventEnrollmentDetailedEvaluationStage::EvaluationState,
520                    EventEnrollmentDetailedEvaluationOutcome::Rejected,
521                ));
522            } else {
523                state_committed = true;
524                if update.clears_invalidation {
525                    db.set_enrollment_eval_state_invalidated(oid, false);
526                }
527            }
528        }
529
530        if update.observation_unavailable {
531            if source_failed || state_failed {
532                db.set_enrollment_eval_state_invalidated(oid, true);
533            }
534            report.diagnostics.push(diagnostic(
535                oid,
536                EventEnrollmentDetailedEvaluationStage::Reliability,
537                EventEnrollmentDetailedEvaluationOutcome::ObservationUnavailable,
538            ));
539            continue;
540        }
541
542        if state_failed {
543            continue;
544        }
545
546        if let Some(reliability) = update.reliability {
547            let coordinate = EventTransition::for_target_state(reliability.to);
548            let staged_timestamp = stage_event_timestamp(db);
549            let change = EventStateChange {
550                from: reliability.from,
551                to: reliability.to,
552            };
553            let transition = EventTransitionCommit {
554                change: change.clone(),
555                coordinate,
556                ack_required: reliability.ack_required,
557                timestamp: staged_timestamp.sample.timestamp.clone(),
558                message_text: None,
559            };
560            let commit = EventEnrollmentReliabilityCommit {
561                reliability: reliability.desired,
562                transition: Some(transition),
563            };
564            let commit_result = db
565                .get_mut(&oid)
566                .map_or(Err(EventTransitionCommitError::Unsupported), |object| {
567                    object.commit_event_enrollment_reliability_internal(commit)
568                });
569
570            if commit_result.is_err() {
571                let outcome = if reliability_or_state_landed(db, &oid, &reliability) {
572                    EventEnrollmentDetailedEvaluationOutcome::LandedAfterError
573                } else {
574                    EventEnrollmentDetailedEvaluationOutcome::Rejected
575                };
576                clear_source_ownership(db, oid, database_eval_sources);
577                db.set_enrollment_eval_state_invalidated(oid, true);
578                report.diagnostics.push(diagnostic(
579                    oid,
580                    EventEnrollmentDetailedEvaluationStage::Reliability,
581                    outcome,
582                ));
583                continue;
584            }
585
586            let recipient_clock = confirm_event_timestamp(db, staged_timestamp).clock;
587            let result = EventEnrollmentReliabilityResult {
588                enrollment_oid: oid,
589                monitored_oid: reliability.monitored_oid,
590                previous_reliability: reliability.previous,
591                new_reliability: reliability.desired,
592                state_change: Some(change),
593                distribute: reliability.distribute,
594                cause: reliability.cause,
595            };
596            report.reliability_results.push(result.clone());
597            let event_type = EventType::CHANGE_OF_RELIABILITY;
598            if let Some(event_values) = project_event_enrollment_payload(
599                db,
600                oid,
601                reliability.monitored_oid,
602                result
603                    .state_change
604                    .as_ref()
605                    .expect("committed Reliability delivery has a state change"),
606                event_type,
607                None,
608                Some(&reliability.referenced_value),
609            ) {
610                deliveries.push(CommittedEventEnrollmentDelivery {
611                    result: CommittedEventEnrollmentResult::Reliability(result),
612                    ack_required: reliability.ack_required,
613                    recipient_clock,
614                    event_type,
615                    event_values,
616                });
617            } else {
618                tracing::debug!(
619                    enrollment = %oid,
620                    "Committed Event Enrollment reliability payload rejected; suppressing distribution"
621                );
622            }
623            continue;
624        }
625
626        let Some(fired) = update.fired else {
627            report.diagnostics.push(diagnostic(
628                oid,
629                EventEnrollmentDetailedEvaluationStage::EvaluationState,
630                if update.canceled && state_committed {
631                    EventEnrollmentDetailedEvaluationOutcome::CancellationCommitted
632                } else {
633                    EventEnrollmentDetailedEvaluationOutcome::NoTransition
634                },
635            ));
636            continue;
637        };
638
639        // A same-state normal-event transition depends on private source
640        // ownership to distinguish the new indication from the previous one.
641        // Reliability re-entry is independently distinguished by its changed
642        // Reliability value and therefore is not suppressed by source failure.
643        if source_failed && fired.from == fired.to {
644            continue;
645        }
646
647        let coordinate = EventTransition::for_target_state(fired.to);
648        let staged_timestamp = stage_event_timestamp(db);
649        let change = EventStateChange {
650            from: fired.from,
651            to: fired.to,
652        };
653        let commit = EventTransitionCommit {
654            change: change.clone(),
655            coordinate,
656            ack_required: fired.ack_required,
657            timestamp: staged_timestamp.sample.timestamp.clone(),
658            message_text: None,
659        };
660        let commit_result = db
661            .get_mut(&oid)
662            .map_or(Err(EventTransitionCommitError::Unsupported), |object| {
663                object.commit_event_transition_internal(commit)
664            });
665
666        if commit_result.is_err() {
667            let outcome = if fired.from != fired.to && event_state_landed(db, &oid, fired.to) {
668                EventEnrollmentDetailedEvaluationOutcome::LandedAfterError
669            } else {
670                EventEnrollmentDetailedEvaluationOutcome::Rejected
671            };
672            clear_source_ownership(db, oid, database_eval_sources);
673            db.set_enrollment_eval_state_invalidated(oid, true);
674            report.diagnostics.push(diagnostic(
675                oid,
676                EventEnrollmentDetailedEvaluationStage::EventTransition,
677                outcome,
678            ));
679            continue;
680        }
681
682        let recipient_clock = confirm_event_timestamp(db, staged_timestamp).clock;
683        let event_type = change.event_type(EventType::from_raw(fired.event_type_raw));
684        let result = EventEnrollmentTransition {
685            enrollment_oid: oid,
686            monitored_oid: fired.monitored_oid,
687            change,
688            event_type,
689            distribute: fired.distribute,
690        };
691        report.transitions.push(result.clone());
692        if let Some(event_values) = project_event_enrollment_payload(
693            db,
694            oid,
695            Some(fired.monitored_oid),
696            &result.change,
697            event_type,
698            Some(&fired.projection),
699            None,
700        ) {
701            deliveries.push(CommittedEventEnrollmentDelivery {
702                result: CommittedEventEnrollmentResult::Normal(result),
703                ack_required: fired.ack_required,
704                recipient_clock,
705                event_type,
706                event_values,
707            });
708        } else {
709            tracing::debug!(
710                enrollment = %oid,
711                ?event_type,
712                "Committed Event Enrollment payload rejected; suppressing distribution"
713            );
714        }
715    }
716
717    EventEnrollmentEvaluationBatch { report, deliveries }
718}