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};
8use core::cmp::Ordering;
9
10use crate::{algebra::ResourceVector, wire::RecordAdmission};
11
12use super::super::{
13    AttachCommit, AttachTransition, BindingState, ClaimFrontiers, ClosureAccounting, ClosureDebt,
14    CommittedDetachTransition, DebtCompletion, DetachCell, DetachedCredentialRecovery, Event,
15    FencedAttachCommit, FrontierBinding, FrontierParticipant, IdentityState,
16    InitialEnrollmentFrontierCommit, LeaveCommitError, LeaveCommitParameters, LiveMember,
17    MarkerAckCommit, NonzeroParticipantAckCommit, ObserverProgressProjection, OrderLedger,
18    ParticipantAckCommit, PendingFinalization, PendingLeaveCommitParameters,
19    PrepareLeaveAuthorityError, RetainedCausalRecord, RetainedCausalRecordKind, SequenceLedger,
20    StoredEdge, VerifiedLeaveRequest,
21    claim_frontier::{
22        BindingTerminalOwner, FencedMarkerSourceRecord, LiveFrontierTransitionError,
23        PrecedenceCondition,
24    },
25    commit_leave, commit_pending_leave,
26};
27use super::{
28    InitialEnrollmentOperationCommit, MarkerDeliveryProjection, MarkerDrainCommit,
29    RecordAdmissionPersistenceParts, RetainedRecordCharge, UnchangedRecordAdmission,
30};
31
32mod binding_fate_transition;
33mod checkpoint_restore;
34pub use checkpoint_restore::{
35    CheckpointParticipant, FrontierCheckpointSource, LiveFrontierRestoreError,
36    RestoredLiveFrontierOwner,
37};
38mod ledger;
39mod state;
40pub(super) use binding_fate_transition::BindingFateOwnerPlan;
41use ledger::{
42    detach_order, detach_sequence, detached_attach_order, detached_attach_sequence,
43    enrollment_order, enrollment_sequence, superseding_attach_order, superseding_attach_sequence,
44};
45use state::{
46    accounting_after_fenced_attach, accounting_after_leave, accounting_after_marker_ack,
47    accounting_after_marker_crossings, accounting_after_marker_remint, accounting_after_rows,
48    retained_attached, retained_terminal,
49};
50
51/// Complete executable frontier, closure-accounting, and keyed-retention owner.
52///
53/// The owner is intentionally move-only. It is the only live mutation input and
54/// restores saved components only against a validated lifecycle capsule.
55/// Frontier, closure, retained charges, and participant history therefore cannot
56/// be cloned or recombined from different owners:
57///
58/// ```compile_fail
59/// use liminal_protocol::lifecycle::LiveFrontierOwner;
60///
61/// fn clone_frontier(owner: &LiveFrontierOwner) -> LiveFrontierOwner {
62///     owner.clone()
63/// }
64/// ```
65///
66/// ```compile_fail
67/// use liminal_protocol::lifecycle::LiveFrontierOwner;
68///
69/// fn splice(left: &mut LiveFrontierOwner, right: LiveFrontierOwner) {
70///     left.frontiers = right.frontiers;
71///     left.closure_accounting = right.closure_accounting;
72///     left.retained_charges = right.retained_charges;
73/// }
74/// ```
75#[derive(Debug, PartialEq, Eq)]
76pub struct LiveFrontierOwner {
77    frontiers: ClaimFrontiers,
78    closure_accounting: ClosureAccounting,
79    retained_charges: Vec<RetainedRecordCharge>,
80    retained_record_limit: u64,
81}
82
83/// Outcome of one marker-anchor ledger reconciliation.
84///
85/// The two ledgers an admission cross-checks are the frontier CENSUS (marker
86/// records whose owning participant is active with a cursor short of the
87/// marker, plus markers still pending as immutable sequence candidates) and
88/// the STORED count in closure accounting. Reconciliation is
89/// census-authoritative: the census is derived from rows that are present, the
90/// stored count from arithmetic that a lost row can no longer correct.
91///
92/// A refused repair is its own arm rather than a zero count. The two are
93/// operationally different — one says the ledgers agree, the other says they
94/// disagree in a way this repair may not touch — and collapsing them would
95/// publish an in-step ledger where a corruption stands.
96#[derive(Clone, Copy, Debug, PartialEq, Eq)]
97pub enum MarkerAnchorReconcile {
98    /// Census and stored accounting already agree; nothing moved.
99    InStep,
100    /// Stored accounting held anchors the census can no longer derive; exactly
101    /// that excess was retired.
102    RetiredOrphans(u64),
103    /// The census derives anchors stored accounting has lost; exactly that
104    /// deficit was re-minted.
105    RemintedDeficit(u64),
106    /// The ledgers disagree and `ClosureAccounting::try_new` refused the
107    /// repair. State is unchanged, in both directions, exactly as before.
108    Refused {
109        /// Anchors the frontier census derives.
110        census: u64,
111        /// Anchors durable closure accounting holds.
112        stored: u64,
113    },
114}
115
116impl MarkerAnchorReconcile {
117    /// Returns how many anchors moved, in either direction.
118    #[must_use]
119    pub const fn reconciled(self) -> u64 {
120        match self {
121            Self::InStep | Self::Refused { .. } => 0,
122            Self::RetiredOrphans(count) | Self::RemintedDeficit(count) => count,
123        }
124    }
125}
126
127impl LiveFrontierOwner {
128    pub(super) fn retain_ordinary_cursor_release(
129        &mut self,
130        source: Box<crate::lifecycle::storage::OrdinaryBindingFateRestore>,
131    ) {
132        self.frontiers.retain_ordinary_cursor_release(source);
133    }
134
135    /// Borrows the accounting scalars; the closure authority remains in the capsule.
136    #[must_use]
137    pub fn encode_closure_accounting_source(&self) -> Vec<u8> {
138        super::super::checkpoint_codec::encode_accounting(&self.closure_accounting)
139    }
140
141    /// Decodes accounting against the capsule's validated closure authority.
142    ///
143    /// # Errors
144    ///
145    /// Retains framing failures and the native accounting validation error.
146    pub fn decode_closure_accounting_source(
147        state: super::super::ClosureState,
148        bytes: &[u8],
149    ) -> Result<ClosureAccounting, super::super::FrontierAccountingDecodeError> {
150        super::super::checkpoint_codec::decode_accounting(state, bytes)
151    }
152
153    /// Borrows every keyed retained charge in canonical order.
154    #[must_use]
155    pub fn encode_retained_charge_source(&self) -> Option<Vec<u8>> {
156        super::super::checkpoint_codec::encode_charges(&self.retained_charges)
157    }
158
159    /// Decodes retained charges without constructing executable frontier authority.
160    ///
161    /// # Errors
162    ///
163    /// Refuses malformed framing, non-unit rows and nonconsecutive or unordered keys.
164    pub fn decode_retained_charge_source(
165        bytes: &[u8],
166    ) -> Result<Vec<RetainedRecordCharge>, super::super::CapsuleDecodeError> {
167        super::super::checkpoint_codec::decode_charges(bytes)
168    }
169
170    /// Acquires live ownership from the protocol's atomic initial-enrollment result.
171    #[must_use]
172    pub fn from_initial_enrollment<F>(
173        initial: InitialEnrollmentFrontierCommit<F>,
174        retained_record_limit: u64,
175    ) -> (InitialEnrollmentOperationCommit<F>, Self) {
176        let (operation, frontiers, closure_accounting, attached_charge) =
177            initial.into_conversation_parts();
178        let attached = operation.enrollment().attached;
179        let retained_charges = vec![RetainedRecordCharge::new(
180            attached.delivery_seq(),
181            attached.admission_order(),
182            attached_charge,
183        )];
184        (
185            operation,
186            Self {
187                frontiers,
188                closure_accounting,
189                retained_charges,
190                retained_record_limit,
191            },
192        )
193    }
194
195    /// Builds a complete owner from independently validated parts.
196    ///
197    /// ⛔ ABSENT FROM PRODUCTION BUILDS (`test-support` / `cfg(test)` only) and
198    /// deliberately so: it is the one way to express a durable state that NO
199    /// live transition can reach, which is exactly what a corruption fixture
200    /// needs. The 2026-08-28 marker-anchor wedge — one active participant, one
201    /// retained unaccepted compaction marker, stored anchors at zero — cannot
202    /// be built any other way, because every live transition moves both marker
203    /// ledgers in one commit. Downstream test trees (the server's) reach it
204    /// through this seam rather than fabricating a second frontier type.
205    #[cfg(any(test, feature = "test-support"))]
206    #[must_use]
207    pub const fn from_test_parts(
208        frontiers: ClaimFrontiers,
209        closure_accounting: ClosureAccounting,
210        retained_charges: Vec<RetainedRecordCharge>,
211        retained_record_limit: u64,
212    ) -> Self {
213        Self {
214            frontiers,
215            closure_accounting,
216            retained_charges,
217            retained_record_limit,
218        }
219    }
220
221    /// Test-support-only checked capacity extension applied after a real
222    /// terminal selector has already chosen Pending. It cannot affect that
223    /// disposition; the caller supplies the exact encoded finalizer charges.
224    ///
225    /// # Errors
226    ///
227    /// Returns an error if exact capacity arithmetic or reconstruction fails.
228    #[cfg(any(test, feature = "test-support"))]
229    pub fn with_pending_finalizer_test_capacity(
230        mut self,
231        finalizer_rows: u64,
232        finalizer_charge: ResourceVector,
233    ) -> Result<Self, &'static str> {
234        let retained_count = u64::try_from(self.retained_charges.len())
235            .map_err(|_| "retained record count exceeds u64")?;
236        self.retained_record_limit = retained_count
237            .checked_add(finalizer_rows)
238            .ok_or("pending finalizer retained-record capacity overflow")?;
239        let current = self.closure_accounting;
240        let configured = current.configured_cap();
241        let configured = ResourceVector::new(
242            configured
243                .entries
244                .checked_add(finalizer_charge.entries)
245                .ok_or("pending finalizer entry capacity overflow")?,
246            configured
247                .bytes
248                .checked_add(finalizer_charge.bytes)
249                .ok_or("pending finalizer byte capacity overflow")?,
250        );
251        self.closure_accounting = ClosureAccounting::try_new(
252            current.state(),
253            current.marker_capacity_credits(),
254            current.marker_anchors(),
255            current.edge_sequence_claims(),
256            current.edge_order_position_claims(),
257            current.edge_k_remaining(),
258            current.baseline(),
259            configured,
260            current.episode_churn_used(),
261            current.episode_churn_limit(),
262        )
263        .map_err(|_| "pending finalizer closure capacity extension refused")?;
264        Ok(self)
265    }
266
267    /// Borrows the coupled claim frontiers.
268    #[must_use]
269    pub const fn frontiers(&self) -> &ClaimFrontiers {
270        &self.frontiers
271    }
272
273    /// Returns complete current closure accounting.
274    #[must_use]
275    pub const fn closure_accounting(&self) -> ClosureAccounting {
276        self.closure_accounting
277    }
278
279    /// Borrows canonical keyed charges for the retained suffix.
280    #[must_use]
281    pub fn retained_charges(&self) -> &[RetainedRecordCharge] {
282        &self.retained_charges
283    }
284
285    /// Returns the signed retained causal-row cap.
286    #[must_use]
287    pub const fn retained_record_limit(&self) -> u64 {
288        self.retained_record_limit
289    }
290
291    /// Reconciles the stored marker-anchor ledger against the frontier census,
292    /// in BOTH directions.
293    ///
294    /// An anchor is derived only while its marker record survives retention
295    /// AND its owning participant is still active with a cursor short of the
296    /// marker; participant erasure or record retirement zeroes the derived
297    /// side with no acknowledgement row ever retiring the stored side. The
298    /// stranded anchor then wedges every later admission on the
299    /// `MarkerAnchorAccounting` cross-check with nothing left in the log that
300    /// could clear it.
301    ///
302    /// ⛔ THE OTHER DIRECTION WAS PINNED AS IMPOSSIBLE, AND IT IS NOT. This
303    /// repair used to retire the stored excess only, leaving a derived count
304    /// at or above the stored one "for the admission projection to fault on".
305    /// That was a true statement about LIVE transitions — every one of them
306    /// moves both ledgers in a single commit — and a false one about the
307    /// system, because this crate's own one-way replay retry mints the
308    /// derived-ahead split by over-retiring at a committed row whose replay
309    /// census under-derives. The 2026-08-28 field incident is that state
310    /// standing on Tom's estate since 2026-08-08 (conversation 1,
311    /// `delivery_seq` 187), refusing every admission, forever.
312    ///
313    /// So the census is the AUTHORITY in both directions: it is derived from
314    /// rows that are present, while the stored count is arithmetic a lost row
315    /// can no longer correct. `stored > census` retires the excess;
316    /// `census > stored` re-mints the deficit. Both are bounded by
317    /// `ClosureAccounting::try_new` — a repair the accounting arithmetic
318    /// refuses leaves the state unchanged, exactly as the retirement direction
319    /// always did.
320    ///
321    /// The re-mint cannot overshoot. The credit cross-check runs BEFORE the
322    /// anchor one and the anchor list is validated as a duplicate-free subset
323    /// of the credits (`ordinary_record_projection.rs:983-998` @ `b4ef9fb`), so
324    /// any state that reaches the anchor fault has already proven
325    /// `derived_anchors <= derived_credits == stored_credits`.
326    ///
327    /// A PLANNED marker still pending as an immutable sequence candidate holds
328    /// its stored anchor before any retained record exists — the `DrainFirst`
329    /// discipline keeps admissions (and their cross-check) out of that window.
330    /// Those anchors are accounted, not orphaned, and are counted in the census
331    /// here the same way.
332    #[must_use]
333    pub fn reconcile_marker_anchor_ledgers(&mut self) -> MarkerAnchorReconcile {
334        let census = self
335            .frontiers
336            .unaccepted_marker_anchor_count()
337            .saturating_add(self.frontiers.pending_marker_candidate_count());
338        let stored = self.closure_accounting.marker_anchors();
339        let repaired = match census.cmp(&stored) {
340            Ordering::Equal => return MarkerAnchorReconcile::InStep,
341            Ordering::Less => stored.checked_sub(census).and_then(|orphaned| {
342                accounting_after_marker_crossings(self.closure_accounting, orphaned)
343                    .map(|accounting| (accounting, MarkerAnchorReconcile::RetiredOrphans(orphaned)))
344            }),
345            Ordering::Greater => census.checked_sub(stored).and_then(|minted| {
346                accounting_after_marker_remint(self.closure_accounting, minted)
347                    .map(|accounting| (accounting, MarkerAnchorReconcile::RemintedDeficit(minted)))
348            }),
349        };
350        match repaired {
351            Some((accounting, outcome)) => {
352                self.closure_accounting = accounting;
353                outcome
354            }
355            None => MarkerAnchorReconcile::Refused { census, stored },
356        }
357    }
358
359    /// Consumes the complete owner for `RecordAdmission`, Leave, or persistence.
360    #[must_use]
361    pub fn into_parts(
362        self,
363    ) -> (
364        ClaimFrontiers,
365        ClosureAccounting,
366        Vec<RetainedRecordCharge>,
367        u64,
368    ) {
369        (
370            self.frontiers,
371            self.closure_accounting,
372            self.retained_charges,
373            self.retained_record_limit,
374        )
375    }
376
377    /// Restores the exact owner returned by a non-committing admission and
378    /// recovers the same request for a same-lock retry.
379    #[must_use]
380    pub fn from_unchanged_record_admission<EF, V, LF>(
381        unchanged: UnchangedRecordAdmission<'_, EF, V, LF>,
382        retained_record_limit: u64,
383    ) -> (Self, RecordAdmission, ResourceVector) {
384        let (prestate, encoded_record_charge) = unchanged.into_parts();
385        let (request, frontiers, closure_accounting, retained_charges) =
386            prestate.into_live_owner_parts();
387        (
388            Self {
389                frontiers,
390                closure_accounting,
391                retained_charges,
392                retained_record_limit,
393            },
394            request,
395            encoded_record_charge,
396        )
397    }
398
399    /// Acquires the exact owner from the complete sealed successful
400    /// `RecordAdmission` persistence authority.
401    #[must_use]
402    pub fn from_record_admission_persistence(
403        persistence: RecordAdmissionPersistenceParts,
404        retained_record_limit: u64,
405    ) -> Self {
406        Self {
407            frontiers: persistence.frontiers,
408            closure_accounting: persistence.accounting,
409            retained_charges: persistence.retained_charges,
410            retained_record_limit,
411        }
412    }
413
414    /// Acquires the exact post-drain owner and durable marker successor.
415    #[must_use]
416    pub fn from_marker_drain(
417        commit: MarkerDrainCommit,
418        retained_record_limit: u64,
419    ) -> (Self, StoredEdge, MarkerDeliveryProjection) {
420        let (frontiers, closure_accounting, retained_charges, successor, projection) =
421            commit.into_parts();
422        (
423            Self {
424                frontiers,
425                closure_accounting,
426                retained_charges,
427                retained_record_limit,
428            },
429            successor,
430            projection,
431        )
432    }
433
434    pub(super) fn commit_binding_terminal_candidate(
435        self,
436        active_binding: super::super::ActiveBinding,
437        admission_order: super::super::AdmissionOrder,
438        delivery_seq: crate::wire::DeliverySeq,
439        charge: RetainedRecordCharge,
440    ) -> Result<Self, Box<(Self, LiveFrontierError)>> {
441        let mut active = self.frontiers.active_identities().participants().to_vec();
442        let Some(participant) = active
443            .iter_mut()
444            .find(|participant| participant.participant_index() == active_binding.participant_id)
445        else {
446            return Err(Box::new((self, LiveFrontierError::Authority)));
447        };
448        if participant.binding() != FrontierBinding::Bound(active_binding.binding_epoch) {
449            return Err(Box::new((self, LiveFrontierError::Authority)));
450        }
451        *participant = FrontierParticipant::new(
452            participant.participant_index(),
453            participant.cursor(),
454            FrontierBinding::Detached(active_binding.binding_epoch),
455        );
456        let row = RetainedCausalRecord {
457            delivery_seq,
458            admission_order,
459            kind: RetainedCausalRecordKind::BindingTerminal(super::super::BindingTerminalOwner {
460                participant_index: active_binding.participant_id,
461                binding_epoch: active_binding.binding_epoch,
462            }),
463        };
464        let Some(sequence) = detach_sequence(self.frontiers.sequence().ledger(), delivery_seq)
465        else {
466            return Err(Box::new((self, LiveFrontierError::Frontier)));
467        };
468        let Some(order) = detach_order(
469            self.frontiers.order().ledger(),
470            admission_order.transaction_order(),
471        ) else {
472            return Err(Box::new((self, LiveFrontierError::Frontier)));
473        };
474        match transition(self, (), active, &[row], vec![charge], sequence, order) {
475            Ok(committed) => {
476                let ((), owner) = committed.into_parts();
477                Ok(owner)
478            }
479            Err(failure) => {
480                let error = failure.error();
481                let ((), owner) = failure.into_parts();
482                Err(Box::new((owner, error)))
483            }
484        }
485    }
486
487    pub(super) fn pend_binding_terminal_candidate(
488        self,
489        active_binding: super::super::ActiveBinding,
490        admission_order: super::super::AdmissionOrder,
491        delivery_seq: crate::wire::DeliverySeq,
492    ) -> Result<Self, Box<(Self, LiveFrontierError)>> {
493        let Some(order) = detach_order(
494            self.frontiers.order().ledger(),
495            admission_order.transaction_order(),
496        ) else {
497            return Err(Box::new((self, LiveFrontierError::Frontier)));
498        };
499        let Self {
500            frontiers,
501            closure_accounting,
502            retained_charges,
503            retained_record_limit,
504        } = self;
505        match frontiers.apply_pending_binding_terminal(
506            active_binding.participant_id,
507            active_binding.binding_epoch,
508            delivery_seq,
509            admission_order,
510            order,
511        ) {
512            Ok(frontiers) => Ok(Self {
513                frontiers,
514                closure_accounting,
515                retained_charges,
516                retained_record_limit,
517            }),
518            Err(failure) => {
519                let (frontiers, error) = *failure;
520                Err(Box::new((
521                    Self {
522                        frontiers,
523                        closure_accounting,
524                        retained_charges,
525                        retained_record_limit,
526                    },
527                    map_frontier_error(error),
528                )))
529            }
530        }
531    }
532
533    /// Commits the exact first pending binding-terminal candidate as its own
534    /// candidate transaction — the R-A2 candidate-lane terminal drain.
535    ///
536    /// The caller supplies only the pending finalization authority it already
537    /// owns and the canonical keyed terminal charge; the candidate itself, its
538    /// delivery sequence, and its already-allocated order major are derived
539    /// from the coupled frontiers. Retention transitions through the same
540    /// accounting the live transitions use, and the resulting owner is exactly
541    /// the poststate an immediately-committed terminal would have produced,
542    /// with one difference by flavor: a drained `Detached` participant stays
543    /// detached at its dead epoch, while a drained `Died` participant leaves
544    /// the active identities and frees its identity slot, as a `Left` would
545    /// (decision of 11 October 2026, matching the server's erasure of a
546    /// drained `Died` slot and token under the 2026-07-23 flavor ruling; see
547    /// `ClaimFrontiers::drain_first_binding_terminal`).
548    ///
549    /// The retained-row CAP is deliberately not re-checked here: the terminal
550    /// pended exactly because the cap could not admit its row at fate time
551    /// ([`PreparedBindingTerminal::admit`](super::PreparedBindingTerminal::admit)),
552    /// and pending DEFERRED that exact reserved row rather than discarding it.
553    /// R-A2 prescribes the drain transaction's retention transition
554    /// unconditionally — an accepted terminal fate can never become
555    /// unsequencable — so the drain honors the deferred reservation even while
556    /// the suffix rests at its cap. The overage is bounded by the
557    /// open-candidate count, itself bounded by the contract's identity-slot
558    /// candidate bound.
559    ///
560    /// # Errors
561    ///
562    /// Returns the unchanged owner when the first candidate is absent or not
563    /// this pending terminal, the charge does not name the candidate's exact
564    /// keyed row, or the resulting retention/closure accounting is invalid.
565    pub fn drain_pending_terminal(
566        self,
567        pending: PendingFinalization,
568        terminal_charge: RetainedRecordCharge,
569    ) -> Result<DrainedPendingTerminal, Box<PendingTerminalDrainRefused>> {
570        let Some(expected_sequence) = self
571            .frontiers
572            .sequence()
573            .ledger()
574            .high_watermark()
575            .checked_add(1)
576        else {
577            return drain_refusal(self, LiveFrontierError::Frontier);
578        };
579        if terminal_charge.delivery_seq() != expected_sequence
580            || terminal_charge.admission_order() != pending.admission_order()
581            || terminal_charge.encoded_charge().entries != 1
582        {
583            return drain_refusal(self, LiveFrontierError::RetainedCharge);
584        }
585        if self
586            .retained_charges
587            .len()
588            .checked_add(1)
589            .and_then(|len| u64::try_from(len).ok())
590            .is_none()
591        {
592            return drain_refusal(self, LiveFrontierError::RetainedRecordLimit);
593        }
594        let Some(accounting) = accounting_after_rows(self.closure_accounting, &[terminal_charge])
595        else {
596            return drain_refusal(self, LiveFrontierError::ClosureAccounting);
597        };
598        let Self {
599            frontiers,
600            closure_accounting,
601            mut retained_charges,
602            retained_record_limit,
603        } = self;
604        let expected_owner = BindingTerminalOwner {
605            participant_index: pending.participant_id(),
606            binding_epoch: pending.binding_epoch(),
607        };
608        let (frontiers, record) = match frontiers.drain_first_binding_terminal(
609            expected_owner,
610            pending.admission_order(),
611            matches!(pending, PendingFinalization::Died(_)),
612        ) {
613            Ok(drained) => drained,
614            Err(failure) => {
615                let (frontiers, error) = *failure;
616                return drain_refusal(
617                    Self {
618                        frontiers,
619                        closure_accounting,
620                        retained_charges,
621                        retained_record_limit,
622                    },
623                    map_frontier_error(error),
624                );
625            }
626        };
627        retained_charges.push(terminal_charge);
628        let projection = ObserverProgressProjection::sourced(
629            pending.conversation_id(),
630            record.delivery_seq,
631            super::super::checkpoint_codec::ObserverProjectionSource::Terminal(
632                pending.commit(record.delivery_seq).storage_description(),
633            ),
634        );
635        Ok(DrainedPendingTerminal {
636            owner: Self {
637                frontiers,
638                closure_accounting: accounting,
639                retained_charges,
640                retained_record_limit,
641            },
642            projection,
643        })
644    }
645
646    /// Retains this move-only owner and the exact recovery while its durable
647    /// marker source row is read and validated.
648    ///
649    /// # Errors
650    /// Returns the unchanged owner and recovery when the fully restored frontier
651    /// does not contain their exact delivered marker occurrence.
652    pub fn retain_fenced_marker_source(
653        self,
654        recovery: DetachedCredentialRecovery,
655    ) -> Result<RetainedFencedMarkerSource, Box<FencedMarkerSourceRetentionRefused>> {
656        let Some(source) = self.frontiers.fenced_marker_source(recovery) else {
657            return Err(Box::new(FencedMarkerSourceRetentionRefused {
658                owner: self,
659                recovery,
660            }));
661        };
662        Ok(RetainedFencedMarkerSource {
663            owner: self,
664            recovery,
665            expectation: FencedMarkerSourceExpectation { source },
666        })
667    }
668
669    /// Consumes the complete owner and the exact descriptive inputs to mint one
670    /// fenced attach proof from the selected retained marker occurrence.
671    ///
672    /// This is the sole public production mint. The caller cannot supply a raw
673    /// marker token: the owner removes it from its fully validated frontiers.
674    /// Refusal returns this owner and every input unchanged after reinstalling
675    /// that same occurrence authority, allowing serial retry but no fork.
676    #[must_use]
677    pub fn mint_fenced_attach(
678        mut self,
679        marker_source_sequence: u64,
680        recovery: DetachedCredentialRecovery,
681        debt: ClosureDebt,
682        event: Event,
683        successor: DebtCompletion,
684    ) -> MintFencedAttachResult {
685        let Some(record) = self.frontiers.take_fenced_marker_record(recovery) else {
686            return MintFencedAttachResult::MintRefused(Box::new(MintFencedAttachRefused {
687                owner: self,
688                marker_source_sequence,
689                recovery,
690                debt,
691                event,
692                successor,
693                reason: FencedAttachMintRefusalReason::MarkerAuthority,
694            }));
695        };
696        let marker_source_bytes = super::super::checkpoint_codec::encode_marker_source(&record);
697        match recovery.fenced_attach(record, debt, event, successor) {
698            Ok(proof) => MintFencedAttachResult::Minted(Box::new(MintedFencedAttach {
699                owner_without_marker_authority: self,
700                proof,
701                marker_source_sequence,
702                marker_source_bytes,
703            })),
704            Err(refusal) => {
705                self.frontiers
706                    .reinstall_fenced_marker_record((*refusal).into_record());
707                MintFencedAttachResult::MintRefused(Box::new(MintFencedAttachRefused {
708                    owner: self,
709                    marker_source_sequence,
710                    recovery,
711                    debt,
712                    event,
713                    successor,
714                    reason: FencedAttachMintRefusalReason::ProofInputs,
715                }))
716            }
717        }
718    }
719}
720
721/// Complete atomic candidate-lane terminal drain commit.
722///
723/// The transitioned owner and the protocol-produced observer projection share
724/// one sealed predecessor and cannot be recombined from unrelated drains.
725#[derive(Debug, PartialEq, Eq)]
726pub struct DrainedPendingTerminal {
727    owner: LiveFrontierOwner,
728    projection: ObserverProgressProjection,
729}
730
731impl DrainedPendingTerminal {
732    /// Consumes the atomic drain into its owner and exact typed projection.
733    #[must_use]
734    pub fn into_parts(self) -> (LiveFrontierOwner, ObserverProgressProjection) {
735        (self.owner, self.projection)
736    }
737}
738
739/// Failed candidate-lane terminal drain retaining the unchanged owner.
740#[derive(Debug, PartialEq, Eq)]
741pub struct PendingTerminalDrainRefused {
742    owner: LiveFrontierOwner,
743    error: LiveFrontierError,
744}
745
746impl PendingTerminalDrainRefused {
747    /// Returns the exact typed refusal.
748    #[must_use]
749    pub const fn error(&self) -> LiveFrontierError {
750        self.error
751    }
752
753    /// Recovers the unchanged complete owner.
754    #[must_use]
755    pub fn into_owner(self) -> LiveFrontierOwner {
756        self.owner
757    }
758}
759
760fn drain_refusal(
761    owner: LiveFrontierOwner,
762    error: LiveFrontierError,
763) -> Result<DrainedPendingTerminal, Box<PendingTerminalDrainRefused>> {
764    Err(Box::new(PendingTerminalDrainRefused { owner, error }))
765}
766
767/// Exact protocol-recomputed marker facts a durable source row must match.
768#[derive(Clone, Copy, Debug, PartialEq, Eq)]
769pub struct FencedMarkerSourceExpectation {
770    source: FencedMarkerSourceRecord,
771}
772
773impl FencedMarkerSourceExpectation {
774    /// Returns the conversation owning the durable marker source.
775    #[must_use]
776    pub const fn conversation_id(self) -> u64 {
777        self.source.conversation_id
778    }
779
780    /// Returns the marker's durable delivery sequence.
781    #[must_use]
782    pub const fn marker_delivery_seq(self) -> u64 {
783        self.source.delivery_seq
784    }
785
786    /// Returns the marker's immutable causal key.
787    #[must_use]
788    pub const fn admission_order(self) -> super::super::AdmissionOrder {
789        self.source.admission_order
790    }
791
792    /// Returns the permanent marker owner.
793    #[must_use]
794    pub const fn participant_id(self) -> u64 {
795        self.source.participant_id
796    }
797
798    /// Returns the marker's immutable provenance.
799    #[must_use]
800    pub const fn provenance(self) -> super::super::MarkerProvenance {
801        self.source.provenance
802    }
803
804    /// Returns the historically validated delivery target.
805    #[must_use]
806    pub const fn target_binding(self) -> FrontierBinding {
807        self.source.target_binding
808    }
809}
810
811/// Move-only owner/recovery pair held across one bounded durable source read.
812#[derive(Debug, PartialEq, Eq)]
813pub struct RetainedFencedMarkerSource {
814    owner: LiveFrontierOwner,
815    recovery: DetachedCredentialRecovery,
816    expectation: FencedMarkerSourceExpectation,
817}
818
819impl RetainedFencedMarkerSource {
820    /// Returns the protocol-recomputed facts the durable row must match.
821    #[must_use]
822    pub const fn expectation(&self) -> FencedMarkerSourceExpectation {
823        self.expectation
824    }
825
826    /// Returns the unchanged owner and recovery after source validation.
827    #[must_use]
828    pub fn into_parts(self) -> (LiveFrontierOwner, DetachedCredentialRecovery) {
829        (self.owner, self.recovery)
830    }
831}
832
833/// Refused source retention with the owner and recovery unchanged.
834#[derive(Debug, PartialEq, Eq)]
835pub struct FencedMarkerSourceRetentionRefused {
836    owner: LiveFrontierOwner,
837    recovery: DetachedCredentialRecovery,
838}
839
840impl FencedMarkerSourceRetentionRefused {
841    /// Returns the unchanged owner and recovery after retention refusal.
842    #[must_use]
843    pub fn into_parts(self) -> (LiveFrontierOwner, DetachedCredentialRecovery) {
844        (self.owner, self.recovery)
845    }
846}
847
848/// Exact reason the owner could not mint a fenced attach proof.
849#[derive(Clone, Copy, Debug, PartialEq, Eq)]
850pub enum FencedAttachMintRefusalReason {
851    /// The validated frontier did not own the selected delivered marker record,
852    /// or that record authority was already consumed.
853    MarkerAuthority,
854    /// Marker authority existed, but recovery/event/successor inputs disagreed.
855    ProofInputs,
856}
857
858/// Successful one-use fenced proof mint.
859#[derive(Debug, PartialEq, Eq)]
860pub struct MintedFencedAttach {
861    marker_source_sequence: u64,
862    marker_source_bytes: Vec<u8>,
863    owner_without_marker_authority: LiveFrontierOwner,
864    proof: FencedAttachCommit,
865}
866
867impl MintedFencedAttach {
868    /// Borrows the exact validated marker source before its proof is consumed.
869    #[must_use]
870    pub fn marker_record_source(&self) -> (u64, &[u8]) {
871        (self.marker_source_sequence, &self.marker_source_bytes)
872    }
873
874    /// Consumes the result into the owner with spent marker authority and the
875    /// sole proof admitted to the downstream by-value chain.
876    #[must_use]
877    pub fn into_parts(self) -> (LiveFrontierOwner, FencedAttachCommit) {
878        (self.owner_without_marker_authority, self.proof)
879    }
880}
881
882/// Failed one-use fenced proof mint with unchanged retry authority and inputs.
883#[derive(Debug, PartialEq, Eq)]
884pub struct MintFencedAttachRefused {
885    owner: LiveFrontierOwner,
886    marker_source_sequence: u64,
887    recovery: DetachedCredentialRecovery,
888    debt: ClosureDebt,
889    event: Event,
890    successor: DebtCompletion,
891    reason: FencedAttachMintRefusalReason,
892}
893
894impl MintFencedAttachRefused {
895    /// Returns the typed refusal cause.
896    #[must_use]
897    pub const fn reason(&self) -> FencedAttachMintRefusalReason {
898        self.reason
899    }
900
901    /// Consumes the refusal into the unchanged owner and all original inputs.
902    #[must_use]
903    pub fn into_parts(
904        self,
905    ) -> (
906        LiveFrontierOwner,
907        u64,
908        DetachedCredentialRecovery,
909        ClosureDebt,
910        Event,
911        DebtCompletion,
912    ) {
913        (
914            self.owner,
915            self.marker_source_sequence,
916            self.recovery,
917            self.debt,
918            self.event,
919            self.successor,
920        )
921    }
922}
923
924/// Complete result of the sole production fenced proof mint.
925#[derive(Debug, PartialEq, Eq)]
926pub enum MintFencedAttachResult {
927    /// Exactly one marker authority was spent and one proof was minted.
928    Minted(Box<MintedFencedAttach>),
929    /// No proof was minted; the same authority and inputs are serially retryable.
930    MintRefused(Box<MintFencedAttachRefused>),
931}
932
933/// Complete move-only settled Leave result: tombstone and executable owner.
934#[derive(Debug, PartialEq, Eq)]
935pub struct LiveLeaveCommit<EF, V, LF> {
936    identity: IdentityState<EF, V, LF>,
937    owner: LiveFrontierOwner,
938}
939
940impl<EF, V, LF> LiveLeaveCommit<EF, V, LF> {
941    /// Projects permanent Leave's exact protocol-committed `Left` sequence.
942    #[must_use]
943    pub const fn observer_progress_projection(&self) -> Option<ObserverProgressProjection> {
944        let IdentityState::Retired(retired) = &self.identity else {
945            return None;
946        };
947        let committed = retired.committed_result();
948        Some(ObserverProgressProjection::sourced(
949            committed.conversation_id(),
950            committed.left_delivery_seq(),
951            super::super::checkpoint_codec::ObserverProjectionSource::Leave(
952                super::super::LeaveCommittedRestore {
953                    conversation_id: committed.conversation_id(),
954                    leave_attempt_token: committed.leave_attempt_token(),
955                    participant_id: committed.participant_id(),
956                    retired_generation: committed.retired_generation(),
957                    ended_binding_epoch: committed.ended_binding_epoch(),
958                    prior_terminal_delivery_seq: committed.prior_terminal_delivery_seq(),
959                    left_delivery_seq: committed.left_delivery_seq(),
960                },
961            ),
962        ))
963    }
964
965    /// Consumes the atomic result into its inseparable tombstone and owner.
966    #[must_use]
967    pub fn into_parts(self) -> (IdentityState<EF, V, LF>, LiveFrontierOwner) {
968        (self.identity, self.owner)
969    }
970}
971
972/// Typed failure of the protocol-owned settled Leave live transition.
973#[derive(Clone, Debug, PartialEq, Eq)]
974pub enum LiveLeaveError {
975    /// Claim-frontier Leave authority could not be prepared.
976    Prepare(PrepareLeaveAuthorityError),
977    /// Membership retirement rejected inconsistent authority.
978    Commit(LeaveCommitError),
979    /// Canonical Left-row charge did not name the protocol-produced row.
980    RetainedCharge,
981    /// Resulting retained-row count exceeded the signed cap.
982    RetainedRecordLimit,
983    /// Resulting closure accounting exceeded configured capacity.
984    ClosureAccounting,
985    /// Leave did not produce a retired identity.
986    Identity,
987}
988
989/// Commits settled bound or detached Leave through one complete live owner.
990///
991/// # Errors
992///
993/// Returns [`LiveLeaveError`] when preparation or retirement authority is
994/// inconsistent, the caller's keyed Left charge does not match the committed
995/// row, or the resulting retention/closure accounting exceeds its authority.
996pub fn commit_settled_leave_frontier<EF, V, LF, D>(
997    owner: LiveFrontierOwner,
998    member: LiveMember<EF>,
999    binding: BindingState,
1000    detach_cell: DetachCell<D>,
1001    verified: VerifiedLeaveRequest<V, LF>,
1002    left_delivery_seq: u64,
1003    left_charge: RetainedRecordCharge,
1004) -> Result<LiveLeaveCommit<EF, V, LF>, LiveLeaveError> {
1005    let LiveFrontierOwner {
1006        frontiers,
1007        closure_accounting,
1008        mut retained_charges,
1009        retained_record_limit,
1010    } = owner;
1011    let retired_marker_charge =
1012        retired_marker_charge(&frontiers, &retained_charges, member.participant_id())?;
1013    let authority = frontiers
1014        .prepare_settled_leave_authority(&member, binding)
1015        .map_err(LiveLeaveError::Prepare)?;
1016    let commit = commit_leave(
1017        member,
1018        binding,
1019        detach_cell,
1020        verified,
1021        authority,
1022        LeaveCommitParameters { left_delivery_seq },
1023    )
1024    .map_err(LiveLeaveError::Commit)?;
1025    let (identity, frontiers) = commit.into_parts();
1026    let IdentityState::Retired(retired) = &identity else {
1027        return Err(LiveLeaveError::Identity);
1028    };
1029    if left_charge.delivery_seq() != retired.committed_result().left_delivery_seq()
1030        || left_charge.admission_order() != retired.left_admission_order()
1031        || left_charge.encoded_charge().entries != 1
1032    {
1033        return Err(LiveLeaveError::RetainedCharge);
1034    }
1035    retained_charges.push(left_charge);
1036    retained_charges.sort_unstable_by_key(|charge| charge.delivery_seq());
1037    let retained_len = u64::try_from(frontiers.retained_records().len())
1038        .map_err(|_| LiveLeaveError::RetainedRecordLimit)?;
1039    if retained_len > retained_record_limit
1040        || retained_charges.len() != frontiers.retained_records().len()
1041    {
1042        return Err(LiveLeaveError::RetainedRecordLimit);
1043    }
1044    let closure_accounting =
1045        accounting_after_leave(closure_accounting, &[left_charge], retired_marker_charge)
1046            .ok_or(LiveLeaveError::ClosureAccounting)?;
1047    Ok(LiveLeaveCommit {
1048        identity,
1049        owner: LiveFrontierOwner {
1050            frontiers,
1051            closure_accounting,
1052            retained_charges,
1053            retained_record_limit,
1054        },
1055    })
1056}
1057
1058/// Commits a pending binding terminal immediately before Leave through one
1059/// complete live owner.
1060///
1061/// # Errors
1062///
1063/// Returns [`LiveLeaveError`] when pending preparation or retirement authority
1064/// is inconsistent, either caller charge does not match its protocol-produced
1065/// row, or resulting retention/closure accounting exceeds its authority.
1066pub fn commit_pending_leave_frontier<EF, V, LF, D>(
1067    owner: LiveFrontierOwner,
1068    member: LiveMember<EF>,
1069    pending: PendingFinalization,
1070    detach_cell: DetachCell<D>,
1071    verified: VerifiedLeaveRequest<V, LF>,
1072    parameters: PendingLeaveCommitParameters,
1073    charges: [RetainedRecordCharge; 2],
1074) -> Result<LiveLeaveCommit<EF, V, LF>, LiveLeaveError> {
1075    let [terminal_charge, left_charge] = charges;
1076    let terminal_delivery_seq = parameters.terminal_delivery_seq;
1077    let LiveFrontierOwner {
1078        frontiers,
1079        closure_accounting,
1080        mut retained_charges,
1081        retained_record_limit,
1082    } = owner;
1083    let retired_marker_charge =
1084        retired_marker_charge(&frontiers, &retained_charges, member.participant_id())?;
1085    let authority = frontiers
1086        .prepare_pending_leave_authority(&member, pending)
1087        .map_err(LiveLeaveError::Prepare)?;
1088    let commit = commit_pending_leave(
1089        member,
1090        pending,
1091        detach_cell,
1092        verified,
1093        authority,
1094        parameters,
1095    )
1096    .map_err(LiveLeaveError::Commit)?;
1097    let (identity, frontiers) = commit.into_parts();
1098    let IdentityState::Retired(retired) = &identity else {
1099        return Err(LiveLeaveError::Identity);
1100    };
1101    if retired.committed_result().prior_terminal_delivery_seq() != Some(terminal_delivery_seq)
1102        || terminal_charge.delivery_seq() != terminal_delivery_seq
1103        || terminal_charge.admission_order() != pending.admission_order()
1104        || terminal_charge.encoded_charge().entries != 1
1105        || left_charge.delivery_seq() != retired.committed_result().left_delivery_seq()
1106        || left_charge.admission_order() != retired.left_admission_order()
1107        || left_charge.encoded_charge().entries != 1
1108    {
1109        return Err(LiveLeaveError::RetainedCharge);
1110    }
1111    retained_charges.extend([terminal_charge, left_charge]);
1112    retained_charges.sort_unstable_by_key(|charge| charge.delivery_seq());
1113    let retained_len = u64::try_from(frontiers.retained_records().len())
1114        .map_err(|_| LiveLeaveError::RetainedRecordLimit)?;
1115    if retained_len > retained_record_limit {
1116        return Err(LiveLeaveError::RetainedRecordLimit);
1117    }
1118    if retained_charges.len() != frontiers.retained_records().len() {
1119        return Err(LiveLeaveError::RetainedCharge);
1120    }
1121    let closure_accounting = accounting_after_leave(
1122        closure_accounting,
1123        &[terminal_charge, left_charge],
1124        retired_marker_charge,
1125    )
1126    .ok_or(LiveLeaveError::ClosureAccounting)?;
1127    Ok(LiveLeaveCommit {
1128        identity,
1129        owner: LiveFrontierOwner {
1130            frontiers,
1131            closure_accounting,
1132            retained_charges,
1133            retained_record_limit,
1134        },
1135    })
1136}
1137
1138fn retired_marker_charge(
1139    frontiers: &ClaimFrontiers,
1140    retained_charges: &[RetainedRecordCharge],
1141    participant_id: crate::wire::ParticipantId,
1142) -> Result<Option<RetainedRecordCharge>, LiveLeaveError> {
1143    let marker_sequence = frontiers
1144        .retained_marker_records()
1145        .iter()
1146        .find_map(|record| {
1147            matches!(
1148                record.kind,
1149                RetainedCausalRecordKind::CompactionMarker {
1150                    participant_index,
1151                    ..
1152                } if participant_index == participant_id
1153            )
1154            .then_some(record.delivery_seq)
1155        });
1156    let Some(marker_sequence) = marker_sequence else {
1157        return Ok(None);
1158    };
1159    retained_charges
1160        .iter()
1161        .copied()
1162        .find(|charge| charge.delivery_seq() == marker_sequence)
1163        .map(Some)
1164        .ok_or(LiveLeaveError::RetainedCharge)
1165}
1166
1167/// Exact charges for a credential attach's one or two retained rows.
1168#[derive(Debug, PartialEq, Eq)]
1169pub struct AttachFrontierCharges {
1170    terminal: Option<RetainedRecordCharge>,
1171    attached: RetainedRecordCharge,
1172    seal: LiveTransitionInputSeal,
1173}
1174
1175#[derive(Debug, PartialEq, Eq)]
1176enum LiveTransitionInputSeal {
1177    Validated,
1178}
1179
1180impl AttachFrontierCharges {
1181    /// Couples the canonical `Attached` charge with an optional terminal charge.
1182    #[must_use]
1183    pub const fn new(
1184        terminal: Option<RetainedRecordCharge>,
1185        attached: RetainedRecordCharge,
1186    ) -> Self {
1187        Self {
1188            terminal,
1189            attached,
1190            seal: LiveTransitionInputSeal::Validated,
1191        }
1192    }
1193
1194    const fn into_parts(self) -> (Option<RetainedRecordCharge>, RetainedRecordCharge) {
1195        let Self {
1196            terminal,
1197            attached,
1198            seal,
1199        } = self;
1200        match seal {
1201            LiveTransitionInputSeal::Validated => (terminal, attached),
1202        }
1203    }
1204}
1205
1206/// A typed lifecycle commit paired with its complete post-transition owner.
1207#[derive(Debug, PartialEq, Eq)]
1208pub struct LiveFrontierCommit<T> {
1209    operation: T,
1210    owner: LiveFrontierOwner,
1211}
1212
1213impl<T> LiveFrontierCommit<T> {
1214    /// Borrows the exact typed lifecycle commit.
1215    #[must_use]
1216    pub const fn operation(&self) -> &T {
1217        &self.operation
1218    }
1219
1220    /// Borrows the complete post-transition owner.
1221    #[must_use]
1222    pub const fn owner(&self) -> &LiveFrontierOwner {
1223        &self.owner
1224    }
1225
1226    /// Consumes the atomic transition for durability publication.
1227    #[must_use]
1228    pub fn into_parts(self) -> (T, LiveFrontierOwner) {
1229        (self.operation, self.owner)
1230    }
1231}
1232
1233/// Failed live transition retaining the unchanged complete owner and operation.
1234#[derive(Debug, PartialEq, Eq)]
1235pub struct LiveFrontierFailure<T> {
1236    error: LiveFrontierError,
1237    operation: T,
1238    owner: LiveFrontierOwner,
1239}
1240
1241impl<T> LiveFrontierFailure<T> {
1242    /// Returns the exact typed transition failure.
1243    #[must_use]
1244    pub const fn error(&self) -> LiveFrontierError {
1245        self.error
1246    }
1247
1248    /// Recovers the unchanged owner and intact operation commit.
1249    #[must_use]
1250    pub fn into_parts(self) -> (T, LiveFrontierOwner) {
1251        (self.operation, self.owner)
1252    }
1253}
1254
1255/// Failure selected while coupling a sealed lifecycle commit to live ownership.
1256#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1257pub enum LiveFrontierError {
1258    /// Commit and live owner name different authority.
1259    Authority,
1260    /// A mandatory immutable/recovery transition has precedence.
1261    ///
1262    /// Carries WHICH of amendment A5's clearing conditions blocked
1263    /// (participant contract §0.16). The law attaches to the SEAM, so the
1264    /// discriminant is produced by `apply_live_transition`'s own guard and
1265    /// carried out through every wrapper — a future wrapper inherits it
1266    /// without being enumerated anywhere.
1267    Precedence(PrecedenceCondition),
1268    /// Canonical keyed row charges differ from the commit-derived retained rows.
1269    RetainedCharge,
1270    /// The retained causal-row cap would be exceeded.
1271    RetainedRecordLimit,
1272    /// Aggregate claim arithmetic or exact owner reconstruction failed.
1273    Frontier,
1274    /// Resulting closure accounting is invalid or outside its signed capacity.
1275    ClosureAccounting,
1276}
1277
1278/// Result of coupling any typed lifecycle commit to live frontier ownership.
1279pub type LiveFrontierResult<T> = Result<LiveFrontierCommit<T>, Box<LiveFrontierFailure<T>>>;
1280
1281/// Applies a subsequent enrollment to the complete live owner.
1282///
1283/// # Errors
1284///
1285/// Returns a failure retaining the unchanged owner and intact enrollment commit.
1286pub fn apply_enrollment_frontier<F>(
1287    owner: LiveFrontierOwner,
1288    operation: super::super::EnrollmentCommit<F>,
1289    attached_charge: RetainedRecordCharge,
1290) -> LiveFrontierResult<super::super::EnrollmentCommit<F>> {
1291    let attached = operation.attached;
1292    if attached.conversation_id() != owner.frontiers.conversation_id() {
1293        return failure(owner, operation, LiveFrontierError::Authority);
1294    }
1295    let participant_id = attached.participant_id();
1296    let mut active = owner.frontiers.active_identities().participants().to_vec();
1297    if active
1298        .iter()
1299        .any(|participant| participant.participant_index() == participant_id)
1300    {
1301        return failure(owner, operation, LiveFrontierError::Authority);
1302    }
1303    active.push(FrontierParticipant::new(
1304        participant_id,
1305        operation.member.cursor(),
1306        FrontierBinding::Bound(attached.binding_epoch()),
1307    ));
1308    active.sort_unstable_by_key(|participant| participant.participant_index());
1309    let rows = [retained_attached(attached)];
1310    let Some(sequence) =
1311        enrollment_sequence(owner.frontiers.sequence().ledger(), attached.delivery_seq())
1312    else {
1313        return failure(owner, operation, LiveFrontierError::Frontier);
1314    };
1315    let Some(order) = enrollment_order(
1316        owner.frontiers.order().ledger(),
1317        attached.admission_order().transaction_order(),
1318    ) else {
1319        return failure(owner, operation, LiveFrontierError::Frontier);
1320    };
1321    transition(
1322        owner,
1323        operation,
1324        active,
1325        &rows,
1326        vec![attached_charge],
1327        sequence,
1328        order,
1329    )
1330}
1331
1332/// Applies credential attach to the complete live owner.
1333///
1334/// # Errors
1335///
1336/// Returns a failure retaining the unchanged owner, intact attach commit, and
1337/// exact reason the commit could not enter the frontier.
1338pub fn apply_attach_frontier<F, V>(
1339    owner: LiveFrontierOwner,
1340    operation: AttachCommit<F, V>,
1341    charges: AttachFrontierCharges,
1342) -> LiveFrontierResult<AttachCommit<F, V>> {
1343    let (terminal_charge, attached_charge) = charges.into_parts();
1344    let attached = operation.attached;
1345    if attached.conversation_id() != owner.frontiers.conversation_id() {
1346        return failure(owner, operation, LiveFrontierError::Authority);
1347    }
1348    let mut active = owner.frontiers.active_identities().participants().to_vec();
1349    let Some(participant) = active
1350        .iter_mut()
1351        .find(|participant| participant.participant_index() == attached.participant_id())
1352    else {
1353        return failure(owner, operation, LiveFrontierError::Authority);
1354    };
1355    let frontier_cursor = participant.cursor();
1356    *participant = FrontierParticipant::new(
1357        participant.participant_index(),
1358        operation.member.cursor(),
1359        FrontierBinding::Bound(attached.binding_epoch()),
1360    );
1361    let reopened = reopened_marker_anchors(
1362        &owner,
1363        attached.participant_id(),
1364        frontier_cursor,
1365        operation.member.cursor(),
1366    );
1367    with_reopened_marker_anchors(owner, operation, reopened, |owner, operation| {
1368        apply_attach_rows(owner, operation, active, terminal_charge, attached_charge)
1369    })
1370}
1371
1372/// Commits a detached or superseding attach's rows once its cursor is placed.
1373fn apply_attach_rows<F, V>(
1374    owner: LiveFrontierOwner,
1375    operation: AttachCommit<F, V>,
1376    active: Vec<FrontierParticipant>,
1377    terminal_charge: Option<RetainedRecordCharge>,
1378    attached_charge: RetainedRecordCharge,
1379) -> LiveFrontierResult<AttachCommit<F, V>> {
1380    let attached = operation.attached;
1381    let current_sequence = owner.frontiers.sequence().ledger();
1382    let current_order = owner.frontiers.order().ledger();
1383    let (rows, keyed_charges, sequence, order) = match operation.transition {
1384        AttachTransition::Detached => {
1385            if terminal_charge.is_some() {
1386                return failure(owner, operation, LiveFrontierError::RetainedCharge);
1387            }
1388            let Some(sequence) =
1389                detached_attach_sequence(current_sequence, attached.delivery_seq())
1390            else {
1391                return failure(owner, operation, LiveFrontierError::Frontier);
1392            };
1393            let Some(order) = detached_attach_order(
1394                current_order,
1395                attached.admission_order().transaction_order(),
1396            ) else {
1397                return failure(owner, operation, LiveFrontierError::Frontier);
1398            };
1399            (
1400                vec![retained_attached(attached)],
1401                vec![attached_charge],
1402                sequence,
1403                order,
1404            )
1405        }
1406        AttachTransition::Superseded { terminal } => {
1407            let Some(terminal_charge) = terminal_charge else {
1408                return failure(owner, operation, LiveFrontierError::RetainedCharge);
1409            };
1410            let rows = vec![
1411                retained_terminal(terminal.into()),
1412                retained_attached(attached),
1413            ];
1414            let Some(sequence) = superseding_attach_sequence(current_sequence, &rows) else {
1415                return failure(owner, operation, LiveFrontierError::Frontier);
1416            };
1417            let Some(order) = superseding_attach_order(
1418                current_order,
1419                attached.admission_order().transaction_order(),
1420            ) else {
1421                return failure(owner, operation, LiveFrontierError::Frontier);
1422            };
1423            (
1424                rows,
1425                vec![terminal_charge, attached_charge],
1426                sequence,
1427                order,
1428            )
1429        }
1430        AttachTransition::FencedRecovery {
1431            prior_binding_epoch,
1432            composed_terminal,
1433            next_closure_state,
1434        } => {
1435            return apply_fenced_attach_frontier(
1436                owner,
1437                operation,
1438                terminal_charge,
1439                attached_charge,
1440                prior_binding_epoch,
1441                composed_terminal,
1442                next_closure_state,
1443            );
1444        }
1445    };
1446    transition(
1447        owner,
1448        operation,
1449        active,
1450        &rows,
1451        keyed_charges,
1452        sequence,
1453        order,
1454    )
1455}
1456
1457/// Applies a committed detach terminal to the complete live owner.
1458///
1459/// # Errors
1460///
1461/// Returns a failure retaining the unchanged owner and intact detach commit.
1462pub fn apply_detach_frontier<EF, V>(
1463    owner: LiveFrontierOwner,
1464    operation: CommittedDetachTransition<EF, V>,
1465    terminal_charge: RetainedRecordCharge,
1466) -> LiveFrontierResult<CommittedDetachTransition<EF, V>> {
1467    let terminal = operation.terminal();
1468    if terminal.conversation_id() != owner.frontiers.conversation_id() {
1469        return failure(owner, operation, LiveFrontierError::Authority);
1470    }
1471    let mut active = owner.frontiers.active_identities().participants().to_vec();
1472    let Some(participant) = active
1473        .iter_mut()
1474        .find(|participant| participant.participant_index() == terminal.participant_id())
1475    else {
1476        return failure(owner, operation, LiveFrontierError::Authority);
1477    };
1478    *participant = FrontierParticipant::new(
1479        participant.participant_index(),
1480        operation.member().cursor(),
1481        FrontierBinding::Detached(terminal.binding_epoch()),
1482    );
1483    let row = retained_terminal(terminal.into());
1484    let Some(sequence) = detach_sequence(owner.frontiers.sequence().ledger(), row.delivery_seq)
1485    else {
1486        return failure(owner, operation, LiveFrontierError::Frontier);
1487    };
1488    let Some(order) = detach_order(
1489        owner.frontiers.order().ledger(),
1490        row.admission_order.transaction_order(),
1491    ) else {
1492        return failure(owner, operation, LiveFrontierError::Frontier);
1493    };
1494    transition(
1495        owner,
1496        operation,
1497        active,
1498        &[row],
1499        vec![terminal_charge],
1500        sequence,
1501        order,
1502    )
1503}
1504
1505/// Counts this participant's active marker anchors that a cumulative cursor
1506/// advance from `previous_cursor` to `through_seq` crosses.
1507///
1508/// A crossed anchor is one that was unaccepted before the advance
1509/// (`previous_cursor < marker seq`) and is accepted after it
1510/// (`marker seq <= through_seq`) — the exact transition the admission
1511/// projection's derived side (`ordinary_unaccepted_marker_anchors`) observes
1512/// from the cursor, which is why the stored accounting must move by the same
1513/// count in the same commit.
1514fn crossed_marker_anchors(
1515    owner: &LiveFrontierOwner,
1516    participant_id: crate::wire::ParticipantId,
1517    previous_cursor: crate::wire::DeliverySeq,
1518    through_seq: crate::wire::DeliverySeq,
1519) -> u64 {
1520    owner
1521        .frontiers
1522        .retained_marker_records()
1523        .iter()
1524        .filter(|record| {
1525            matches!(
1526                record.kind,
1527                RetainedCausalRecordKind::CompactionMarker { participant_index, .. }
1528                    if participant_index == participant_id
1529            ) && previous_cursor < record.delivery_seq
1530                && record.delivery_seq <= through_seq
1531        })
1532        .fold(0_u64, |count, _| count.saturating_add(1))
1533}
1534
1535/// Applies a zero-debt participant acknowledgement cursor transition.
1536///
1537/// A cumulative ack whose `through_seq` crosses this participant's delivered
1538/// compaction marker accepts it, so the marker's anchor is retired from the
1539/// closure accounting in the same commit — the ordinary-path sibling of
1540/// [`apply_marker_ack_frontier`]. Left unretired, the stored anchor count and
1541/// the cursor-derived one diverge and every later admission faults
1542/// (`MarkerAnchorAccounting`), which the dispatch funnel presents as a silent
1543/// connection close (the 2026-08-07 manifold wedge).
1544///
1545/// # Errors
1546///
1547/// Returns a failure retaining the unchanged owner and intact ack commit.
1548pub fn apply_participant_ack_frontier(
1549    mut owner: LiveFrontierOwner,
1550    operation: ParticipantAckCommit,
1551) -> LiveFrontierResult<ParticipantAckCommit> {
1552    let request = operation.outcome().request();
1553    let Some(current) = owner
1554        .frontiers
1555        .active_identities()
1556        .participants()
1557        .iter()
1558        .find(|participant| participant.participant_index() == request.participant_id)
1559        .copied()
1560    else {
1561        return failure(owner, operation, LiveFrontierError::Authority);
1562    };
1563    let crossed = crossed_marker_anchors(
1564        &owner,
1565        request.participant_id,
1566        current.cursor(),
1567        request.through_seq,
1568    );
1569    let accounting = if crossed == 0 {
1570        None
1571    } else {
1572        match accounting_after_marker_crossings(owner.closure_accounting, crossed) {
1573            Some(accounting) => Some(accounting),
1574            None => {
1575                return failure(owner, operation, LiveFrontierError::ClosureAccounting);
1576            }
1577        }
1578    };
1579    let participant = FrontierParticipant::new(
1580        request.participant_id,
1581        request.through_seq,
1582        current.binding(),
1583    );
1584    owner.frontiers = match owner.frontiers.apply_live_identity(participant) {
1585        Ok(frontiers) => frontiers,
1586        Err(frontier_failure) => {
1587            let (frontiers, error) = *frontier_failure;
1588            owner.frontiers = frontiers;
1589            return failure(owner, operation, map_frontier_error(error));
1590        }
1591    };
1592    if let Some(accounting) = accounting {
1593        owner.closure_accounting = accounting;
1594    }
1595    Ok(LiveFrontierCommit { operation, owner })
1596}
1597
1598/// Applies a nonzero-debt participant acknowledgement cursor transition.
1599///
1600/// The episode and member remain owned by the sealed aggregate commit; this
1601/// transition consumes the same exact acknowledged cursor into the coupled
1602/// claim-frontier participant rank.
1603///
1604/// # Errors
1605///
1606/// Returns a failure retaining the unchanged owner and intact aggregate commit.
1607pub fn apply_nonzero_participant_ack_frontier(
1608    mut owner: LiveFrontierOwner,
1609    operation: NonzeroParticipantAckCommit,
1610) -> LiveFrontierResult<NonzeroParticipantAckCommit> {
1611    let request = operation.outcome().request();
1612    let Some(current) = owner
1613        .frontiers
1614        .active_identities()
1615        .participants()
1616        .iter()
1617        .find(|participant| participant.participant_index() == request.participant_id)
1618        .copied()
1619    else {
1620        return failure(owner, operation, LiveFrontierError::Authority);
1621    };
1622    // A nonzero-debt cumulative ack crosses markers exactly as the zero-debt
1623    // one does; the anchor accounting moves with the cursor here too.
1624    let crossed = crossed_marker_anchors(
1625        &owner,
1626        request.participant_id,
1627        current.cursor(),
1628        request.through_seq,
1629    );
1630    let accounting = if crossed == 0 {
1631        None
1632    } else {
1633        match accounting_after_marker_crossings(owner.closure_accounting, crossed) {
1634            Some(accounting) => Some(accounting),
1635            None => {
1636                return failure(owner, operation, LiveFrontierError::ClosureAccounting);
1637            }
1638        }
1639    };
1640    let participant = FrontierParticipant::new(
1641        request.participant_id,
1642        request.through_seq,
1643        current.binding(),
1644    );
1645    owner.frontiers = match owner.frontiers.apply_live_identity(participant) {
1646        Ok(frontiers) => frontiers,
1647        Err(frontier_failure) => {
1648            let (frontiers, error) = *frontier_failure;
1649            owner.frontiers = frontiers;
1650            return failure(owner, operation, map_frontier_error(error));
1651        }
1652    };
1653    if let Some(accounting) = accounting {
1654        owner.closure_accounting = accounting;
1655    }
1656    Ok(LiveFrontierCommit { operation, owner })
1657}
1658
1659/// Applies a zero-debt marker acknowledgement cursor transition.
1660///
1661/// # Errors
1662///
1663/// Returns a failure retaining the unchanged owner and intact marker-ack commit.
1664pub fn apply_marker_ack_frontier(
1665    mut owner: LiveFrontierOwner,
1666    operation: MarkerAckCommit,
1667) -> LiveFrontierResult<MarkerAckCommit> {
1668    let request = operation.outcome().request();
1669    if !owner
1670        .frontiers
1671        .retained_marker_records()
1672        .iter()
1673        .any(|record| {
1674            record.delivery_seq == request.marker_delivery_seq
1675                && matches!(
1676                    record.kind,
1677                    RetainedCausalRecordKind::CompactionMarker { participant_index, .. }
1678                        if participant_index == request.participant_id
1679                )
1680        })
1681    {
1682        return failure(owner, operation, LiveFrontierError::Authority);
1683    }
1684    let Some(current) = owner
1685        .frontiers
1686        .active_identities()
1687        .participants()
1688        .iter()
1689        .find(|participant| participant.participant_index() == request.participant_id)
1690        .copied()
1691    else {
1692        return failure(owner, operation, LiveFrontierError::Authority);
1693    };
1694    let Some(accounting) = accounting_after_marker_ack(owner.closure_accounting) else {
1695        return failure(owner, operation, LiveFrontierError::ClosureAccounting);
1696    };
1697    let participant = FrontierParticipant::new(
1698        request.participant_id,
1699        request.marker_delivery_seq,
1700        current.binding(),
1701    );
1702    owner.frontiers = match owner.frontiers.apply_live_identity(participant) {
1703        Ok(frontiers) => frontiers,
1704        Err(frontier_failure) => {
1705            let (frontiers, error) = *frontier_failure;
1706            owner.frontiers = frontiers;
1707            return failure(owner, operation, map_frontier_error(error));
1708        }
1709    };
1710    owner.closure_accounting = accounting;
1711    Ok(LiveFrontierCommit { operation, owner })
1712}
1713
1714fn apply_fenced_attach_frontier<F, V>(
1715    owner: LiveFrontierOwner,
1716    operation: AttachCommit<F, V>,
1717    terminal_charge: Option<RetainedRecordCharge>,
1718    attached_charge: RetainedRecordCharge,
1719    prior_binding_epoch: crate::wire::BindingEpoch,
1720    composed_terminal: Option<super::super::CommittedBindingTerminal>,
1721    next_closure_state: super::super::ClosureState,
1722) -> LiveFrontierResult<AttachCommit<F, V>> {
1723    let attached = operation.attached;
1724    let (rows, charges) = match (composed_terminal, terminal_charge) {
1725        (None, None) => (vec![retained_attached(attached)], vec![attached_charge]),
1726        (Some(terminal), Some(terminal_charge)) => (
1727            vec![retained_terminal(terminal), retained_attached(attached)],
1728            vec![terminal_charge, attached_charge],
1729        ),
1730        (None, Some(_)) | (Some(_), None) => {
1731            return failure(owner, operation, LiveFrontierError::RetainedCharge);
1732        }
1733    };
1734    let participant = FrontierParticipant::new(
1735        attached.participant_id(),
1736        operation.member.cursor(),
1737        FrontierBinding::Bound(attached.binding_epoch()),
1738    );
1739    fenced_attach_transition(
1740        owner,
1741        operation,
1742        participant,
1743        prior_binding_epoch,
1744        next_closure_state,
1745        &rows,
1746        charges,
1747    )
1748}
1749
1750/// Counts the marker anchors an attach re-opens by placing its participant's
1751/// cursor below where the frontier held it.
1752///
1753/// An attach installs the MEMBER's cursor, the participant's durable
1754/// acknowledged boundary. A connection fate does not move that boundary: it
1755/// carries only the frontier's copy of the cursor to the high watermark
1756/// (`ClaimFrontiers::prepare_binding_fate_transition`), and retires the anchors
1757/// of every marker that carry passes. When the same participant attaches
1758/// again, its cursor returns to the boundary it really holds, and every
1759/// marker of its own between that boundary and the carried cursor is
1760/// unaccepted again — the census derives its anchor once more. The stored
1761/// ledger has to re-mint exactly those anchors in the same commit, or the
1762/// census runs one ahead of it and every boot after re-mints the deficit at
1763/// load, the mirror image of the orphans the fate used to leave.
1764///
1765/// Zero whenever the attach does not lower the cursor, which is every attach
1766/// that no non-finalizer fate preceded.
1767fn reopened_marker_anchors(
1768    owner: &LiveFrontierOwner,
1769    participant_id: crate::wire::ParticipantId,
1770    frontier_cursor: crate::wire::DeliverySeq,
1771    member_cursor: crate::wire::DeliverySeq,
1772) -> u64 {
1773    if member_cursor >= frontier_cursor {
1774        return 0;
1775    }
1776    crossed_marker_anchors(owner, participant_id, member_cursor, frontier_cursor)
1777}
1778
1779/// Runs an attach transition over closure accounting that holds the anchors it
1780/// re-opens, and hands back the unchanged owner if the transition refuses.
1781fn with_reopened_marker_anchors<T>(
1782    mut owner: LiveFrontierOwner,
1783    operation: T,
1784    reopened: u64,
1785    apply: impl FnOnce(LiveFrontierOwner, T) -> LiveFrontierResult<T>,
1786) -> LiveFrontierResult<T> {
1787    if reopened == 0 {
1788        return apply(owner, operation);
1789    }
1790    let unchanged = owner.closure_accounting;
1791    let Some(accounting) = accounting_after_marker_remint(unchanged, reopened) else {
1792        return failure(owner, operation, LiveFrontierError::ClosureAccounting);
1793    };
1794    owner.closure_accounting = accounting;
1795    apply(owner, operation).map_err(|mut refused| {
1796        refused.owner.closure_accounting = unchanged;
1797        refused
1798    })
1799}
1800
1801fn fenced_attach_transition<T>(
1802    mut owner: LiveFrontierOwner,
1803    operation: T,
1804    participant: FrontierParticipant,
1805    prior_binding_epoch: crate::wire::BindingEpoch,
1806    next_closure_state: super::super::ClosureState,
1807    rows: &[RetainedCausalRecord],
1808    charges: Vec<RetainedRecordCharge>,
1809) -> LiveFrontierResult<T> {
1810    if rows.len() != charges.len()
1811        || rows.iter().zip(&charges).any(|(row, charge)| {
1812            row.delivery_seq != charge.delivery_seq()
1813                || row.admission_order != charge.admission_order()
1814                || charge.encoded_charge().entries != 1
1815        })
1816    {
1817        return failure(owner, operation, LiveFrontierError::RetainedCharge);
1818    }
1819    let resulting_len = owner
1820        .frontiers
1821        .retained_records()
1822        .len()
1823        .checked_add(rows.len());
1824    if resulting_len
1825        .and_then(|len| u64::try_from(len).ok())
1826        .is_none_or(|len| len > owner.retained_record_limit)
1827    {
1828        return failure(owner, operation, LiveFrontierError::RetainedRecordLimit);
1829    }
1830    let Some(accounting) =
1831        accounting_after_fenced_attach(owner.closure_accounting, &charges, next_closure_state)
1832    else {
1833        return failure(owner, operation, LiveFrontierError::ClosureAccounting);
1834    };
1835    owner.frontiers =
1836        match owner
1837            .frontiers
1838            .apply_live_fenced_attach(participant, prior_binding_epoch, rows)
1839        {
1840            Ok(frontiers) => frontiers,
1841            Err(frontier_failure) => {
1842                let (frontiers, error) = *frontier_failure;
1843                owner.frontiers = frontiers;
1844                return failure(owner, operation, map_frontier_error(error));
1845            }
1846        };
1847    owner.retained_charges.extend(charges);
1848    owner
1849        .retained_charges
1850        .sort_unstable_by_key(|charge| charge.delivery_seq());
1851    owner.closure_accounting = accounting;
1852    Ok(LiveFrontierCommit { operation, owner })
1853}
1854
1855fn transition<T>(
1856    mut owner: LiveFrontierOwner,
1857    operation: T,
1858    active: Vec<FrontierParticipant>,
1859    rows: &[RetainedCausalRecord],
1860    charges: Vec<RetainedRecordCharge>,
1861    sequence: SequenceLedger,
1862    order: OrderLedger,
1863) -> LiveFrontierResult<T> {
1864    if rows.len() != charges.len()
1865        || rows.iter().zip(&charges).any(|(row, charge)| {
1866            row.delivery_seq != charge.delivery_seq()
1867                || row.admission_order != charge.admission_order()
1868                || charge.encoded_charge().entries != 1
1869        })
1870    {
1871        return failure(owner, operation, LiveFrontierError::RetainedCharge);
1872    }
1873    let resulting_len = owner
1874        .frontiers
1875        .retained_records()
1876        .len()
1877        .checked_add(rows.len());
1878    if resulting_len
1879        .and_then(|len| u64::try_from(len).ok())
1880        .is_none_or(|len| len > owner.retained_record_limit)
1881    {
1882        return failure(owner, operation, LiveFrontierError::RetainedRecordLimit);
1883    }
1884    let Some(accounting) = accounting_after_rows(owner.closure_accounting, &charges) else {
1885        return failure(owner, operation, LiveFrontierError::ClosureAccounting);
1886    };
1887    owner.frontiers = match owner
1888        .frontiers
1889        .apply_live_transition(active, rows, sequence, order)
1890    {
1891        Ok(frontiers) => frontiers,
1892        Err(frontier_failure) => {
1893            let (frontiers, error) = *frontier_failure;
1894            owner.frontiers = frontiers;
1895            return failure(owner, operation, map_frontier_error(error));
1896        }
1897    };
1898    owner.retained_charges.extend(charges);
1899    owner.closure_accounting = accounting;
1900    Ok(LiveFrontierCommit { operation, owner })
1901}
1902
1903const fn map_frontier_error(error: LiveFrontierTransitionError) -> LiveFrontierError {
1904    match error {
1905        LiveFrontierTransitionError::Authority => LiveFrontierError::Authority,
1906        LiveFrontierTransitionError::Precedence(condition) => {
1907            LiveFrontierError::Precedence(condition)
1908        }
1909        LiveFrontierTransitionError::RecordPosition
1910        | LiveFrontierTransitionError::Exhausted
1911        | LiveFrontierTransitionError::ResultingFrontier => LiveFrontierError::Frontier,
1912    }
1913}
1914
1915fn failure<T, U>(
1916    owner: LiveFrontierOwner,
1917    operation: T,
1918    error: LiveFrontierError,
1919) -> Result<U, Box<LiveFrontierFailure<T>>> {
1920    Err(Box::new(LiveFrontierFailure {
1921        error,
1922        operation,
1923        owner,
1924    }))
1925}