Skip to main content

liminal_protocol/lifecycle/operations/
live_frontier.rs

1//! Move-only executable ownership for live lifecycle frontier transitions.
2//!
3//! Storage supplies canonical encoded row charges, but every participant,
4//! binding, cursor, retained row, and aggregate-claim transition is derived
5//! inside the protocol from an existing sealed operation commit.
6
7use alloc::{boxed::Box, vec, vec::Vec};
8
9use crate::{algebra::ResourceVector, wire::RecordAdmission};
10
11use super::super::{
12    AttachCommit, AttachTransition, BindingState, ClaimFrontiers, ClosureAccounting,
13    CommittedDetachTransition, DetachCell, FrontierBinding, FrontierParticipant, IdentityState,
14    InitialEnrollmentFrontierCommit, LeaveCommitError, LeaveCommitParameters, LiveMember,
15    MarkerAckCommit, NonzeroParticipantAckCommit, ObserverProgressProjection, OrderLedger,
16    ParticipantAckCommit, PendingFinalization, PendingLeaveCommitParameters,
17    PrepareLeaveAuthorityError, RetainedCausalRecord, RetainedCausalRecordKind, SequenceLedger,
18    StoredEdge, VerifiedLeaveRequest, claim_frontier::LiveFrontierTransitionError, commit_leave,
19    commit_pending_leave,
20};
21use super::{
22    InitialEnrollmentOperationCommit, MarkerDeliveryProjection, MarkerDrainCommit,
23    RecordAdmissionPersistenceParts, RetainedRecordCharge, UnchangedRecordAdmission,
24};
25
26mod ledger;
27mod state;
28use ledger::{
29    detach_order, detach_sequence, detached_attach_order, detached_attach_sequence,
30    enrollment_order, enrollment_sequence, superseding_attach_order, superseding_attach_sequence,
31};
32use state::{
33    accounting_after_fenced_attach, accounting_after_leave, accounting_after_marker_ack,
34    accounting_after_rows, retained_attached, retained_terminal,
35};
36
37/// Complete executable frontier, closure-accounting, and keyed-retention owner.
38///
39/// The owner is intentionally move-only. It is the only live mutation input and
40/// never exposes a constructor from independent frontier/accounting components.
41/// Frontier, closure, retained charges, and participant history therefore cannot
42/// be cloned or recombined from different owners:
43///
44/// ```compile_fail
45/// use liminal_protocol::lifecycle::LiveFrontierOwner;
46///
47/// fn clone_frontier(owner: &LiveFrontierOwner) -> LiveFrontierOwner {
48///     owner.clone()
49/// }
50/// ```
51///
52/// ```compile_fail
53/// use liminal_protocol::lifecycle::LiveFrontierOwner;
54///
55/// fn splice(left: &mut LiveFrontierOwner, right: LiveFrontierOwner) {
56///     left.frontiers = right.frontiers;
57///     left.closure_accounting = right.closure_accounting;
58///     left.retained_charges = right.retained_charges;
59/// }
60/// ```
61#[derive(Debug, PartialEq, Eq)]
62pub struct LiveFrontierOwner {
63    frontiers: ClaimFrontiers,
64    closure_accounting: ClosureAccounting,
65    retained_charges: Vec<RetainedRecordCharge>,
66    retained_record_limit: u64,
67}
68
69impl LiveFrontierOwner {
70    /// Acquires live ownership from the protocol's atomic initial-enrollment result.
71    #[must_use]
72    pub fn from_initial_enrollment<F>(
73        initial: InitialEnrollmentFrontierCommit<F>,
74        retained_record_limit: u64,
75    ) -> (InitialEnrollmentOperationCommit<F>, Self) {
76        let (operation, frontiers, closure_accounting, attached_charge) =
77            initial.into_conversation_parts();
78        let attached = operation.enrollment().attached;
79        let retained_charges = vec![RetainedRecordCharge::new(
80            attached.delivery_seq(),
81            attached.admission_order(),
82            attached_charge,
83        )];
84        (
85            operation,
86            Self {
87                frontiers,
88                closure_accounting,
89                retained_charges,
90                retained_record_limit,
91            },
92        )
93    }
94
95    #[cfg(test)]
96    pub(in crate::lifecycle) const fn from_test_parts(
97        frontiers: ClaimFrontiers,
98        closure_accounting: ClosureAccounting,
99        retained_charges: Vec<RetainedRecordCharge>,
100        retained_record_limit: u64,
101    ) -> Self {
102        Self {
103            frontiers,
104            closure_accounting,
105            retained_charges,
106            retained_record_limit,
107        }
108    }
109
110    /// Borrows the coupled claim frontiers.
111    #[must_use]
112    pub const fn frontiers(&self) -> &ClaimFrontiers {
113        &self.frontiers
114    }
115
116    /// Returns complete current closure accounting.
117    #[must_use]
118    pub const fn closure_accounting(&self) -> ClosureAccounting {
119        self.closure_accounting
120    }
121
122    /// Borrows canonical keyed charges for the retained suffix.
123    #[must_use]
124    pub fn retained_charges(&self) -> &[RetainedRecordCharge] {
125        &self.retained_charges
126    }
127
128    /// Returns the signed retained causal-row cap.
129    #[must_use]
130    pub const fn retained_record_limit(&self) -> u64 {
131        self.retained_record_limit
132    }
133
134    /// Consumes the complete owner for `RecordAdmission`, Leave, or persistence.
135    #[must_use]
136    pub fn into_parts(
137        self,
138    ) -> (
139        ClaimFrontiers,
140        ClosureAccounting,
141        Vec<RetainedRecordCharge>,
142        u64,
143    ) {
144        (
145            self.frontiers,
146            self.closure_accounting,
147            self.retained_charges,
148            self.retained_record_limit,
149        )
150    }
151
152    /// Restores the exact owner returned by a non-committing admission and
153    /// recovers the same request for a same-lock retry.
154    #[must_use]
155    pub fn from_unchanged_record_admission<EF, V, LF>(
156        unchanged: UnchangedRecordAdmission<'_, EF, V, LF>,
157        retained_record_limit: u64,
158    ) -> (Self, RecordAdmission, ResourceVector) {
159        let (prestate, encoded_record_charge) = unchanged.into_parts();
160        let (request, frontiers, closure_accounting, retained_charges) =
161            prestate.into_live_owner_parts();
162        (
163            Self {
164                frontiers,
165                closure_accounting,
166                retained_charges,
167                retained_record_limit,
168            },
169            request,
170            encoded_record_charge,
171        )
172    }
173
174    /// Acquires the exact owner from the complete sealed successful
175    /// `RecordAdmission` persistence authority.
176    #[must_use]
177    pub fn from_record_admission_persistence(
178        persistence: RecordAdmissionPersistenceParts,
179        retained_record_limit: u64,
180    ) -> Self {
181        Self {
182            frontiers: persistence.frontiers,
183            closure_accounting: persistence.accounting,
184            retained_charges: persistence.retained_charges,
185            retained_record_limit,
186        }
187    }
188
189    /// Acquires the exact post-drain owner and durable marker successor.
190    #[must_use]
191    pub fn from_marker_drain(
192        commit: MarkerDrainCommit,
193        retained_record_limit: u64,
194    ) -> (Self, StoredEdge, MarkerDeliveryProjection) {
195        let (frontiers, closure_accounting, retained_charges, successor, projection) =
196            commit.into_parts();
197        (
198            Self {
199                frontiers,
200                closure_accounting,
201                retained_charges,
202                retained_record_limit,
203            },
204            successor,
205            projection,
206        )
207    }
208}
209
210/// Complete move-only settled Leave result: tombstone and executable owner.
211#[derive(Debug, PartialEq, Eq)]
212pub struct LiveLeaveCommit<EF, V, LF> {
213    identity: IdentityState<EF, V, LF>,
214    owner: LiveFrontierOwner,
215}
216
217impl<EF, V, LF> LiveLeaveCommit<EF, V, LF> {
218    /// Projects permanent Leave's exact protocol-committed `Left` sequence.
219    #[must_use]
220    pub const fn observer_progress_projection(&self) -> Option<ObserverProgressProjection> {
221        let IdentityState::Retired(retired) = &self.identity else {
222            return None;
223        };
224        let committed = retired.committed_result();
225        Some(ObserverProgressProjection::new(
226            committed.conversation_id(),
227            committed.left_delivery_seq(),
228        ))
229    }
230
231    /// Consumes the atomic result into its inseparable tombstone and owner.
232    #[must_use]
233    pub fn into_parts(self) -> (IdentityState<EF, V, LF>, LiveFrontierOwner) {
234        (self.identity, self.owner)
235    }
236}
237
238/// Typed failure of the protocol-owned settled Leave live transition.
239#[derive(Clone, Debug, PartialEq, Eq)]
240pub enum LiveLeaveError {
241    /// Claim-frontier Leave authority could not be prepared.
242    Prepare(PrepareLeaveAuthorityError),
243    /// Membership retirement rejected inconsistent authority.
244    Commit(LeaveCommitError),
245    /// Canonical Left-row charge did not name the protocol-produced row.
246    RetainedCharge,
247    /// Resulting retained-row count exceeded the signed cap.
248    RetainedRecordLimit,
249    /// Resulting closure accounting exceeded configured capacity.
250    ClosureAccounting,
251    /// Leave did not produce a retired identity.
252    Identity,
253}
254
255/// Commits settled bound or detached Leave through one complete live owner.
256///
257/// # Errors
258///
259/// Returns [`LiveLeaveError`] when preparation or retirement authority is
260/// inconsistent, the caller's keyed Left charge does not match the committed
261/// row, or the resulting retention/closure accounting exceeds its authority.
262pub fn commit_settled_leave_frontier<EF, V, LF, D>(
263    owner: LiveFrontierOwner,
264    member: LiveMember<EF>,
265    binding: BindingState,
266    detach_cell: DetachCell<D>,
267    verified: VerifiedLeaveRequest<V, LF>,
268    left_delivery_seq: u64,
269    left_charge: RetainedRecordCharge,
270) -> Result<LiveLeaveCommit<EF, V, LF>, LiveLeaveError> {
271    let LiveFrontierOwner {
272        frontiers,
273        closure_accounting,
274        mut retained_charges,
275        retained_record_limit,
276    } = owner;
277    let retired_marker_charge =
278        retired_marker_charge(&frontiers, &retained_charges, member.participant_id())?;
279    let authority = frontiers
280        .prepare_settled_leave_authority(&member, binding)
281        .map_err(LiveLeaveError::Prepare)?;
282    let commit = commit_leave(
283        member,
284        binding,
285        detach_cell,
286        verified,
287        authority,
288        LeaveCommitParameters { left_delivery_seq },
289    )
290    .map_err(LiveLeaveError::Commit)?;
291    let (identity, frontiers) = commit.into_parts();
292    let IdentityState::Retired(retired) = &identity else {
293        return Err(LiveLeaveError::Identity);
294    };
295    if left_charge.delivery_seq() != retired.committed_result().left_delivery_seq()
296        || left_charge.admission_order() != retired.left_admission_order()
297        || left_charge.encoded_charge().entries != 1
298    {
299        return Err(LiveLeaveError::RetainedCharge);
300    }
301    retained_charges.push(left_charge);
302    retained_charges.sort_unstable_by_key(|charge| charge.delivery_seq());
303    let retained_len = u64::try_from(frontiers.retained_records().len())
304        .map_err(|_| LiveLeaveError::RetainedRecordLimit)?;
305    if retained_len > retained_record_limit
306        || retained_charges.len() != frontiers.retained_records().len()
307    {
308        return Err(LiveLeaveError::RetainedRecordLimit);
309    }
310    let closure_accounting =
311        accounting_after_leave(closure_accounting, &[left_charge], retired_marker_charge)
312            .ok_or(LiveLeaveError::ClosureAccounting)?;
313    Ok(LiveLeaveCommit {
314        identity,
315        owner: LiveFrontierOwner {
316            frontiers,
317            closure_accounting,
318            retained_charges,
319            retained_record_limit,
320        },
321    })
322}
323
324/// Commits a pending binding terminal immediately before Leave through one
325/// complete live owner.
326///
327/// # Errors
328///
329/// Returns [`LiveLeaveError`] when pending preparation or retirement authority
330/// is inconsistent, either caller charge does not match its protocol-produced
331/// row, or resulting retention/closure accounting exceeds its authority.
332pub fn commit_pending_leave_frontier<EF, V, LF, D>(
333    owner: LiveFrontierOwner,
334    member: LiveMember<EF>,
335    pending: PendingFinalization,
336    detach_cell: DetachCell<D>,
337    verified: VerifiedLeaveRequest<V, LF>,
338    parameters: PendingLeaveCommitParameters,
339    charges: [RetainedRecordCharge; 2],
340) -> Result<LiveLeaveCommit<EF, V, LF>, LiveLeaveError> {
341    let [terminal_charge, left_charge] = charges;
342    let terminal_delivery_seq = parameters.terminal_delivery_seq;
343    let LiveFrontierOwner {
344        frontiers,
345        closure_accounting,
346        mut retained_charges,
347        retained_record_limit,
348    } = owner;
349    let retired_marker_charge =
350        retired_marker_charge(&frontiers, &retained_charges, member.participant_id())?;
351    let authority = frontiers
352        .prepare_pending_leave_authority(&member, pending)
353        .map_err(LiveLeaveError::Prepare)?;
354    let commit = commit_pending_leave(
355        member,
356        pending,
357        detach_cell,
358        verified,
359        authority,
360        parameters,
361    )
362    .map_err(LiveLeaveError::Commit)?;
363    let (identity, frontiers) = commit.into_parts();
364    let IdentityState::Retired(retired) = &identity else {
365        return Err(LiveLeaveError::Identity);
366    };
367    if retired.committed_result().prior_terminal_delivery_seq() != Some(terminal_delivery_seq)
368        || terminal_charge.delivery_seq() != terminal_delivery_seq
369        || terminal_charge.admission_order() != pending.admission_order()
370        || terminal_charge.encoded_charge().entries != 1
371        || left_charge.delivery_seq() != retired.committed_result().left_delivery_seq()
372        || left_charge.admission_order() != retired.left_admission_order()
373        || left_charge.encoded_charge().entries != 1
374    {
375        return Err(LiveLeaveError::RetainedCharge);
376    }
377    retained_charges.extend([terminal_charge, left_charge]);
378    retained_charges.sort_unstable_by_key(|charge| charge.delivery_seq());
379    let retained_len = u64::try_from(frontiers.retained_records().len())
380        .map_err(|_| LiveLeaveError::RetainedRecordLimit)?;
381    if retained_len > retained_record_limit
382        || retained_charges.len() != frontiers.retained_records().len()
383    {
384        return Err(LiveLeaveError::RetainedRecordLimit);
385    }
386    let closure_accounting = accounting_after_leave(
387        closure_accounting,
388        &[terminal_charge, left_charge],
389        retired_marker_charge,
390    )
391    .ok_or(LiveLeaveError::ClosureAccounting)?;
392    Ok(LiveLeaveCommit {
393        identity,
394        owner: LiveFrontierOwner {
395            frontiers,
396            closure_accounting,
397            retained_charges,
398            retained_record_limit,
399        },
400    })
401}
402
403fn retired_marker_charge(
404    frontiers: &ClaimFrontiers,
405    retained_charges: &[RetainedRecordCharge],
406    participant_id: crate::wire::ParticipantId,
407) -> Result<Option<RetainedRecordCharge>, LiveLeaveError> {
408    let marker_sequence = frontiers
409        .retained_marker_records()
410        .iter()
411        .find_map(|record| {
412            matches!(
413                record.kind,
414                RetainedCausalRecordKind::CompactionMarker {
415                    participant_index,
416                    ..
417                } if participant_index == participant_id
418            )
419            .then_some(record.delivery_seq)
420        });
421    let Some(marker_sequence) = marker_sequence else {
422        return Ok(None);
423    };
424    retained_charges
425        .iter()
426        .copied()
427        .find(|charge| charge.delivery_seq() == marker_sequence)
428        .map(Some)
429        .ok_or(LiveLeaveError::RetainedCharge)
430}
431
432/// Exact charges for a credential attach's one or two retained rows.
433#[derive(Debug, PartialEq, Eq)]
434pub struct AttachFrontierCharges {
435    terminal: Option<RetainedRecordCharge>,
436    attached: RetainedRecordCharge,
437    seal: LiveTransitionInputSeal,
438}
439
440#[derive(Debug, PartialEq, Eq)]
441enum LiveTransitionInputSeal {
442    Validated,
443}
444
445impl AttachFrontierCharges {
446    /// Couples the canonical `Attached` charge with an optional terminal charge.
447    #[must_use]
448    pub const fn new(
449        terminal: Option<RetainedRecordCharge>,
450        attached: RetainedRecordCharge,
451    ) -> Self {
452        Self {
453            terminal,
454            attached,
455            seal: LiveTransitionInputSeal::Validated,
456        }
457    }
458
459    const fn into_parts(self) -> (Option<RetainedRecordCharge>, RetainedRecordCharge) {
460        let Self {
461            terminal,
462            attached,
463            seal,
464        } = self;
465        match seal {
466            LiveTransitionInputSeal::Validated => (terminal, attached),
467        }
468    }
469}
470
471/// A typed lifecycle commit paired with its complete post-transition owner.
472#[derive(Debug, PartialEq, Eq)]
473pub struct LiveFrontierCommit<T> {
474    operation: T,
475    owner: LiveFrontierOwner,
476}
477
478impl<T> LiveFrontierCommit<T> {
479    /// Borrows the exact typed lifecycle commit.
480    #[must_use]
481    pub const fn operation(&self) -> &T {
482        &self.operation
483    }
484
485    /// Borrows the complete post-transition owner.
486    #[must_use]
487    pub const fn owner(&self) -> &LiveFrontierOwner {
488        &self.owner
489    }
490
491    /// Consumes the atomic transition for durability publication.
492    #[must_use]
493    pub fn into_parts(self) -> (T, LiveFrontierOwner) {
494        (self.operation, self.owner)
495    }
496}
497
498/// Failed live transition retaining the unchanged complete owner and operation.
499#[derive(Debug, PartialEq, Eq)]
500pub struct LiveFrontierFailure<T> {
501    error: LiveFrontierError,
502    operation: T,
503    owner: LiveFrontierOwner,
504}
505
506impl<T> LiveFrontierFailure<T> {
507    /// Returns the exact typed transition failure.
508    #[must_use]
509    pub const fn error(&self) -> LiveFrontierError {
510        self.error
511    }
512
513    /// Recovers the unchanged owner and intact operation commit.
514    #[must_use]
515    pub fn into_parts(self) -> (T, LiveFrontierOwner) {
516        (self.operation, self.owner)
517    }
518}
519
520/// Failure selected while coupling a sealed lifecycle commit to live ownership.
521#[derive(Clone, Copy, Debug, PartialEq, Eq)]
522pub enum LiveFrontierError {
523    /// Commit and live owner name different authority.
524    Authority,
525    /// A mandatory immutable/recovery transition has precedence.
526    Precedence,
527    /// Canonical keyed row charges differ from the commit-derived retained rows.
528    RetainedCharge,
529    /// The retained causal-row cap would be exceeded.
530    RetainedRecordLimit,
531    /// Aggregate claim arithmetic or exact owner reconstruction failed.
532    Frontier,
533    /// Resulting closure accounting is invalid or outside its signed capacity.
534    ClosureAccounting,
535}
536
537/// Result of coupling any typed lifecycle commit to live frontier ownership.
538pub type LiveFrontierResult<T> = Result<LiveFrontierCommit<T>, Box<LiveFrontierFailure<T>>>;
539
540/// Applies a subsequent enrollment to the complete live owner.
541///
542/// # Errors
543///
544/// Returns a failure retaining the unchanged owner and intact enrollment commit.
545pub fn apply_enrollment_frontier<F>(
546    owner: LiveFrontierOwner,
547    operation: super::super::EnrollmentCommit<F>,
548    attached_charge: RetainedRecordCharge,
549) -> LiveFrontierResult<super::super::EnrollmentCommit<F>> {
550    let attached = operation.attached;
551    if attached.conversation_id() != owner.frontiers.conversation_id() {
552        return failure(owner, operation, LiveFrontierError::Authority);
553    }
554    let participant_id = attached.participant_id();
555    let mut active = owner.frontiers.active_identities().participants().to_vec();
556    if active
557        .iter()
558        .any(|participant| participant.participant_index() == participant_id)
559    {
560        return failure(owner, operation, LiveFrontierError::Authority);
561    }
562    active.push(FrontierParticipant::new(
563        participant_id,
564        operation.member.cursor(),
565        FrontierBinding::Bound(attached.binding_epoch()),
566    ));
567    active.sort_unstable_by_key(|participant| participant.participant_index());
568    let rows = [retained_attached(attached)];
569    let Some(sequence) =
570        enrollment_sequence(owner.frontiers.sequence().ledger(), attached.delivery_seq())
571    else {
572        return failure(owner, operation, LiveFrontierError::Frontier);
573    };
574    let Some(order) = enrollment_order(
575        owner.frontiers.order().ledger(),
576        attached.admission_order().transaction_order(),
577    ) else {
578        return failure(owner, operation, LiveFrontierError::Frontier);
579    };
580    transition(
581        owner,
582        operation,
583        active,
584        &rows,
585        vec![attached_charge],
586        sequence,
587        order,
588    )
589}
590
591/// Applies credential attach to the complete live owner.
592///
593/// # Errors
594///
595/// Returns a failure retaining the unchanged owner, intact attach commit, and
596/// exact reason the commit could not enter the frontier.
597pub fn apply_attach_frontier<F, V>(
598    owner: LiveFrontierOwner,
599    operation: AttachCommit<F, V>,
600    charges: AttachFrontierCharges,
601) -> LiveFrontierResult<AttachCommit<F, V>> {
602    let (terminal_charge, attached_charge) = charges.into_parts();
603    let attached = operation.attached;
604    if attached.conversation_id() != owner.frontiers.conversation_id() {
605        return failure(owner, operation, LiveFrontierError::Authority);
606    }
607    let mut active = owner.frontiers.active_identities().participants().to_vec();
608    let Some(participant) = active
609        .iter_mut()
610        .find(|participant| participant.participant_index() == attached.participant_id())
611    else {
612        return failure(owner, operation, LiveFrontierError::Authority);
613    };
614    *participant = FrontierParticipant::new(
615        participant.participant_index(),
616        operation.member.cursor(),
617        FrontierBinding::Bound(attached.binding_epoch()),
618    );
619    let current_sequence = owner.frontiers.sequence().ledger();
620    let current_order = owner.frontiers.order().ledger();
621    let (rows, keyed_charges, sequence, order) = match operation.transition {
622        AttachTransition::Detached => {
623            if terminal_charge.is_some() {
624                return failure(owner, operation, LiveFrontierError::RetainedCharge);
625            }
626            let Some(sequence) =
627                detached_attach_sequence(current_sequence, attached.delivery_seq())
628            else {
629                return failure(owner, operation, LiveFrontierError::Frontier);
630            };
631            let Some(order) = detached_attach_order(
632                current_order,
633                attached.admission_order().transaction_order(),
634            ) else {
635                return failure(owner, operation, LiveFrontierError::Frontier);
636            };
637            (
638                vec![retained_attached(attached)],
639                vec![attached_charge],
640                sequence,
641                order,
642            )
643        }
644        AttachTransition::Superseded { terminal } => {
645            let Some(terminal_charge) = terminal_charge else {
646                return failure(owner, operation, LiveFrontierError::RetainedCharge);
647            };
648            let rows = vec![
649                retained_terminal(terminal.into()),
650                retained_attached(attached),
651            ];
652            let Some(sequence) = superseding_attach_sequence(current_sequence, &rows) else {
653                return failure(owner, operation, LiveFrontierError::Frontier);
654            };
655            let Some(order) = superseding_attach_order(
656                current_order,
657                attached.admission_order().transaction_order(),
658            ) else {
659                return failure(owner, operation, LiveFrontierError::Frontier);
660            };
661            (
662                rows,
663                vec![terminal_charge, attached_charge],
664                sequence,
665                order,
666            )
667        }
668        AttachTransition::FencedRecovery {
669            prior_binding_epoch,
670            composed_terminal,
671            next_closure_state,
672        } => {
673            return apply_fenced_attach_frontier(
674                owner,
675                operation,
676                terminal_charge,
677                attached_charge,
678                prior_binding_epoch,
679                composed_terminal,
680                next_closure_state,
681            );
682        }
683    };
684    transition(
685        owner,
686        operation,
687        active,
688        &rows,
689        keyed_charges,
690        sequence,
691        order,
692    )
693}
694
695/// Applies a committed detach terminal to the complete live owner.
696///
697/// # Errors
698///
699/// Returns a failure retaining the unchanged owner and intact detach commit.
700pub fn apply_detach_frontier<EF, V>(
701    owner: LiveFrontierOwner,
702    operation: CommittedDetachTransition<EF, V>,
703    terminal_charge: RetainedRecordCharge,
704) -> LiveFrontierResult<CommittedDetachTransition<EF, V>> {
705    let terminal = operation.terminal();
706    if terminal.conversation_id() != owner.frontiers.conversation_id() {
707        return failure(owner, operation, LiveFrontierError::Authority);
708    }
709    let mut active = owner.frontiers.active_identities().participants().to_vec();
710    let Some(participant) = active
711        .iter_mut()
712        .find(|participant| participant.participant_index() == terminal.participant_id())
713    else {
714        return failure(owner, operation, LiveFrontierError::Authority);
715    };
716    *participant = FrontierParticipant::new(
717        participant.participant_index(),
718        operation.member().cursor(),
719        FrontierBinding::Detached(terminal.binding_epoch()),
720    );
721    let row = retained_terminal(terminal.into());
722    let Some(sequence) = detach_sequence(owner.frontiers.sequence().ledger(), row.delivery_seq)
723    else {
724        return failure(owner, operation, LiveFrontierError::Frontier);
725    };
726    let Some(order) = detach_order(
727        owner.frontiers.order().ledger(),
728        row.admission_order.transaction_order(),
729    ) else {
730        return failure(owner, operation, LiveFrontierError::Frontier);
731    };
732    transition(
733        owner,
734        operation,
735        active,
736        &[row],
737        vec![terminal_charge],
738        sequence,
739        order,
740    )
741}
742
743/// Applies a zero-debt participant acknowledgement cursor transition.
744///
745/// # Errors
746///
747/// Returns a failure retaining the unchanged owner and intact ack commit.
748pub fn apply_participant_ack_frontier(
749    mut owner: LiveFrontierOwner,
750    operation: ParticipantAckCommit,
751) -> LiveFrontierResult<ParticipantAckCommit> {
752    let request = operation.outcome().request();
753    let Some(current) = owner
754        .frontiers
755        .active_identities()
756        .participants()
757        .iter()
758        .find(|participant| participant.participant_index() == request.participant_id)
759        .copied()
760    else {
761        return failure(owner, operation, LiveFrontierError::Authority);
762    };
763    let participant = FrontierParticipant::new(
764        request.participant_id,
765        request.through_seq,
766        current.binding(),
767    );
768    owner.frontiers = match owner.frontiers.apply_live_identity(participant) {
769        Ok(frontiers) => frontiers,
770        Err(frontier_failure) => {
771            let (frontiers, error) = *frontier_failure;
772            owner.frontiers = frontiers;
773            return failure(owner, operation, map_frontier_error(error));
774        }
775    };
776    Ok(LiveFrontierCommit { operation, owner })
777}
778
779/// Applies a nonzero-debt participant acknowledgement cursor transition.
780///
781/// The episode and member remain owned by the sealed aggregate commit; this
782/// transition consumes the same exact acknowledged cursor into the coupled
783/// claim-frontier participant rank.
784///
785/// # Errors
786///
787/// Returns a failure retaining the unchanged owner and intact aggregate commit.
788pub fn apply_nonzero_participant_ack_frontier(
789    mut owner: LiveFrontierOwner,
790    operation: NonzeroParticipantAckCommit,
791) -> LiveFrontierResult<NonzeroParticipantAckCommit> {
792    let request = operation.outcome().request();
793    let Some(current) = owner
794        .frontiers
795        .active_identities()
796        .participants()
797        .iter()
798        .find(|participant| participant.participant_index() == request.participant_id)
799        .copied()
800    else {
801        return failure(owner, operation, LiveFrontierError::Authority);
802    };
803    let participant = FrontierParticipant::new(
804        request.participant_id,
805        request.through_seq,
806        current.binding(),
807    );
808    owner.frontiers = match owner.frontiers.apply_live_identity(participant) {
809        Ok(frontiers) => frontiers,
810        Err(frontier_failure) => {
811            let (frontiers, error) = *frontier_failure;
812            owner.frontiers = frontiers;
813            return failure(owner, operation, map_frontier_error(error));
814        }
815    };
816    Ok(LiveFrontierCommit { operation, owner })
817}
818
819/// Applies a zero-debt marker acknowledgement cursor transition.
820///
821/// # Errors
822///
823/// Returns a failure retaining the unchanged owner and intact marker-ack commit.
824pub fn apply_marker_ack_frontier(
825    mut owner: LiveFrontierOwner,
826    operation: MarkerAckCommit,
827) -> LiveFrontierResult<MarkerAckCommit> {
828    let request = operation.outcome().request();
829    if !owner
830        .frontiers
831        .retained_marker_records()
832        .iter()
833        .any(|record| {
834            record.delivery_seq == request.marker_delivery_seq
835                && matches!(
836                    record.kind,
837                    RetainedCausalRecordKind::CompactionMarker { participant_index, .. }
838                        if participant_index == request.participant_id
839                )
840        })
841    {
842        return failure(owner, operation, LiveFrontierError::Authority);
843    }
844    let Some(current) = owner
845        .frontiers
846        .active_identities()
847        .participants()
848        .iter()
849        .find(|participant| participant.participant_index() == request.participant_id)
850        .copied()
851    else {
852        return failure(owner, operation, LiveFrontierError::Authority);
853    };
854    let Some(accounting) = accounting_after_marker_ack(owner.closure_accounting) else {
855        return failure(owner, operation, LiveFrontierError::ClosureAccounting);
856    };
857    let participant = FrontierParticipant::new(
858        request.participant_id,
859        request.marker_delivery_seq,
860        current.binding(),
861    );
862    owner.frontiers = match owner.frontiers.apply_live_identity(participant) {
863        Ok(frontiers) => frontiers,
864        Err(frontier_failure) => {
865            let (frontiers, error) = *frontier_failure;
866            owner.frontiers = frontiers;
867            return failure(owner, operation, map_frontier_error(error));
868        }
869    };
870    owner.closure_accounting = accounting;
871    Ok(LiveFrontierCommit { operation, owner })
872}
873
874fn apply_fenced_attach_frontier<F, V>(
875    owner: LiveFrontierOwner,
876    operation: AttachCommit<F, V>,
877    terminal_charge: Option<RetainedRecordCharge>,
878    attached_charge: RetainedRecordCharge,
879    prior_binding_epoch: crate::wire::BindingEpoch,
880    composed_terminal: Option<super::super::CommittedBindingTerminal>,
881    next_closure_state: super::super::ClosureState,
882) -> LiveFrontierResult<AttachCommit<F, V>> {
883    let attached = operation.attached;
884    let (rows, charges) = match (composed_terminal, terminal_charge) {
885        (None, None) => (vec![retained_attached(attached)], vec![attached_charge]),
886        (Some(terminal), Some(terminal_charge)) => (
887            vec![retained_terminal(terminal), retained_attached(attached)],
888            vec![terminal_charge, attached_charge],
889        ),
890        (None, Some(_)) | (Some(_), None) => {
891            return failure(owner, operation, LiveFrontierError::RetainedCharge);
892        }
893    };
894    let participant = FrontierParticipant::new(
895        attached.participant_id(),
896        operation.member.cursor(),
897        FrontierBinding::Bound(attached.binding_epoch()),
898    );
899    fenced_attach_transition(
900        owner,
901        operation,
902        participant,
903        prior_binding_epoch,
904        next_closure_state,
905        &rows,
906        charges,
907    )
908}
909
910fn fenced_attach_transition<T>(
911    mut owner: LiveFrontierOwner,
912    operation: T,
913    participant: FrontierParticipant,
914    prior_binding_epoch: crate::wire::BindingEpoch,
915    next_closure_state: super::super::ClosureState,
916    rows: &[RetainedCausalRecord],
917    charges: Vec<RetainedRecordCharge>,
918) -> LiveFrontierResult<T> {
919    if rows.len() != charges.len()
920        || rows.iter().zip(&charges).any(|(row, charge)| {
921            row.delivery_seq != charge.delivery_seq()
922                || row.admission_order != charge.admission_order()
923                || charge.encoded_charge().entries != 1
924        })
925    {
926        return failure(owner, operation, LiveFrontierError::RetainedCharge);
927    }
928    let resulting_len = owner
929        .frontiers
930        .retained_records()
931        .len()
932        .checked_add(rows.len());
933    if resulting_len
934        .and_then(|len| u64::try_from(len).ok())
935        .is_none_or(|len| len > owner.retained_record_limit)
936    {
937        return failure(owner, operation, LiveFrontierError::RetainedRecordLimit);
938    }
939    let Some(accounting) =
940        accounting_after_fenced_attach(owner.closure_accounting, &charges, next_closure_state)
941    else {
942        return failure(owner, operation, LiveFrontierError::ClosureAccounting);
943    };
944    owner.frontiers =
945        match owner
946            .frontiers
947            .apply_live_fenced_attach(participant, prior_binding_epoch, rows)
948        {
949            Ok(frontiers) => frontiers,
950            Err(frontier_failure) => {
951                let (frontiers, error) = *frontier_failure;
952                owner.frontiers = frontiers;
953                return failure(owner, operation, map_frontier_error(error));
954            }
955        };
956    owner.retained_charges.extend(charges);
957    owner
958        .retained_charges
959        .sort_unstable_by_key(|charge| charge.delivery_seq());
960    owner.closure_accounting = accounting;
961    Ok(LiveFrontierCommit { operation, owner })
962}
963
964fn transition<T>(
965    mut owner: LiveFrontierOwner,
966    operation: T,
967    active: Vec<FrontierParticipant>,
968    rows: &[RetainedCausalRecord],
969    charges: Vec<RetainedRecordCharge>,
970    sequence: SequenceLedger,
971    order: OrderLedger,
972) -> LiveFrontierResult<T> {
973    if rows.len() != charges.len()
974        || rows.iter().zip(&charges).any(|(row, charge)| {
975            row.delivery_seq != charge.delivery_seq()
976                || row.admission_order != charge.admission_order()
977                || charge.encoded_charge().entries != 1
978        })
979    {
980        return failure(owner, operation, LiveFrontierError::RetainedCharge);
981    }
982    let resulting_len = owner
983        .frontiers
984        .retained_records()
985        .len()
986        .checked_add(rows.len());
987    if resulting_len
988        .and_then(|len| u64::try_from(len).ok())
989        .is_none_or(|len| len > owner.retained_record_limit)
990    {
991        return failure(owner, operation, LiveFrontierError::RetainedRecordLimit);
992    }
993    let Some(accounting) = accounting_after_rows(owner.closure_accounting, &charges) else {
994        return failure(owner, operation, LiveFrontierError::ClosureAccounting);
995    };
996    owner.frontiers = match owner
997        .frontiers
998        .apply_live_transition(active, rows, sequence, order)
999    {
1000        Ok(frontiers) => frontiers,
1001        Err(frontier_failure) => {
1002            let (frontiers, error) = *frontier_failure;
1003            owner.frontiers = frontiers;
1004            return failure(owner, operation, map_frontier_error(error));
1005        }
1006    };
1007    owner.retained_charges.extend(charges);
1008    owner.closure_accounting = accounting;
1009    Ok(LiveFrontierCommit { operation, owner })
1010}
1011
1012const fn map_frontier_error(error: LiveFrontierTransitionError) -> LiveFrontierError {
1013    match error {
1014        LiveFrontierTransitionError::Authority => LiveFrontierError::Authority,
1015        LiveFrontierTransitionError::Precedence => LiveFrontierError::Precedence,
1016        LiveFrontierTransitionError::RecordPosition
1017        | LiveFrontierTransitionError::Exhausted
1018        | LiveFrontierTransitionError::ResultingFrontier => LiveFrontierError::Frontier,
1019    }
1020}
1021
1022fn failure<T, U>(
1023    owner: LiveFrontierOwner,
1024    operation: T,
1025    error: LiveFrontierError,
1026) -> Result<U, Box<LiveFrontierFailure<T>>> {
1027    Err(Box::new(LiveFrontierFailure {
1028        error,
1029        operation,
1030        owner,
1031    }))
1032}