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