Skip to main content

liminal_protocol/lifecycle/
storage.rs

1//! Validated durable-state restoration for participant lifecycle typestates.
2//!
3//! Storage serialization necessarily crosses the crate's compile-time state
4//! boundary. These capsules retain the predecessor data needed to rebuild
5//! opaque authorities, then validate every identity, generation, epoch, and
6//! paired-state invariant before returning executable lifecycle values.
7
8use alloc::vec::Vec;
9
10use crate::algebra::WideResourceVector;
11use crate::outcome::ParticipantStateCorruptReason;
12use crate::wire::{
13    AttachSecret, BindingEpoch, CloseCause, ConversationId, DeliverySeq, DetachAttemptToken,
14    Generation, LeaveAttemptToken, LeaveCommitted, ObserverEpoch, ParticipantId, TransactionOrder,
15};
16
17use super::{
18    ActiveBinding, AdmissionOrder, BindingOrigin, BindingState, BoundParticipantCursor,
19    ClaimFrontiers, ClaimFrontiersRestore, ClosureDebt, ClosureState, CommittedBindingTerminal,
20    DebtCompletion, DetachCell, DetachedCredentialRecovery, DetachedMarkerRelease,
21    DetachedMarkerReleaseProvenance, EmptyDetach, EnrollmentFingerprint, Event, FencedAttachCommit,
22    FrontierBinding, IdentityState, LeaveFingerprint, LiveMember, LiveMemberRestore,
23    MarkerDelivery, NonzeroDebtCursorEpisode, ObserverProjection, OrderLedger,
24    OrdinaryBindingAuthority, OrdinaryBindingFate, ParticipantCursorProgress, PendingFinalization,
25    PendingRecoveredCursorRelease, PhysicalCompaction, RecoveredBindingFate,
26    RecoveredBindingFateTransition, RetiredIdentity, SequenceLedger, StoredEdge,
27    binding::{restore_committed_terminal, restore_pending_finalization},
28    claim_frontier::{
29        ClaimFrontiersDescription, HistoricalCausalAuthority, MarkerRecordOccurrence,
30        MarkerRecordRequest, ValidatedConversationHistory, ValidatedMarkerRecord,
31    },
32    cursor_facts::{CursorProgressFact, CursorProgressKey},
33    detach::{
34        restore_committed_detach, restore_pending_detach, restore_terminalized_detach,
35        validate_pending_pair,
36    },
37};
38
39/// A durable lifecycle capsule failed a protocol invariant.
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41pub enum StorageRestoreError {
42    /// A raw committed binding terminal has an impossible cause or restart suffix.
43    CommittedBindingTerminal,
44    /// A pending terminal has an impossible cause or restart suffix.
45    PendingFinalization,
46    /// A binding slot names another live identity or generation.
47    BindingAuthority,
48    /// Live membership disagrees with its decoded terminal identity or generation.
49    MembershipInvariant,
50    /// A raw permanent Leave result is internally inconsistent.
51    LeaveResult,
52    /// A retired identity disagrees with its permanent Leave result.
53    RetiredIdentity,
54    /// A detach-cell variant is internally inconsistent.
55    DetachCell,
56    /// Binding, membership, terminal history, and detach cell do not form one state.
57    DetachBindingPair,
58    /// Cursor-episode floor, participant, or fact state is inconsistent.
59    CursorEpisode,
60    /// A closure state paired a zero debt vector with a stored edge.
61    ClosureDebt,
62    /// A stored edge disagrees with the predecessor provenance in its capsule.
63    StoredEdgeProvenance,
64    /// A carried fenced-marker issuance latch names a take the restored state
65    /// could not have granted: the closure edge is not the detached credential
66    /// recovery of the latched participant, marker and prior epoch, or that
67    /// marker occurrence is no longer active for the participant detached on
68    /// that epoch.
69    FencedMarkerLatch,
70}
71
72/// Failure while jointly restoring claim frontiers and their current closure edge.
73#[derive(Clone, Debug, PartialEq, Eq)]
74pub enum ConversationStateRestoreError {
75    /// Numeric, causal, or cross-counter claim-frontier validation failed.
76    ClaimFrontier(ParticipantStateCorruptReason),
77    /// Raw lifecycle storage disagreed with its typed predecessor authority.
78    Storage(StorageRestoreError),
79}
80
81/// Crate-internal jointly restored claim frontiers and exact current closure state.
82///
83/// This value can execute restored edge authority. It is deliberately absent
84/// from the public storage surface: external callers may deserialize inert
85/// restore data, but only protocol-owned replay may promote it to executable
86/// state.
87#[derive(Debug, PartialEq, Eq)]
88pub(super) struct RestoredConversationState {
89    frontiers: ClaimFrontiers,
90    closure: ClosureState,
91}
92
93/// Complete raw participant-conversation snapshot for public cold restore.
94///
95/// Every field is inert storage data. The only consumer is [`Self::restore`],
96/// which validates the whole snapshot as one unit: participant capsules first,
97/// then the conversation history derived from those exact restored
98/// participants (never an empty history), then frontiers and the closure edge
99/// against that owned history. Components restored from different snapshots
100/// are not independently combinable — [`ParticipantConversationState`] has no
101/// public constructor, restored binding origins and marker-record authorities
102/// are minted only inside this joint validation, and mixing capsules from
103/// different histories fails the frontier-projection and provenance checks.
104/// A valid producer proof therefore cannot be spliced into executable
105/// authority it never had.
106#[derive(Clone, Debug, PartialEq, Eq)]
107pub struct ParticipantConversationRestore<EF, V, LF, D> {
108    /// Complete live and retired participant states.
109    pub participants: Vec<ParticipantLifecycleRestore<EF, V, LF, D>>,
110    /// Coupled raw claim frontiers.
111    pub frontiers: ClaimFrontiersRestore,
112    /// Aggregate delivery-sequence ledger.
113    pub sequence_ledger: SequenceLedger,
114    /// Aggregate transaction-order ledger.
115    pub order_ledger: OrderLedger,
116    /// Current closure state.
117    pub closure: ClosureStateRestore,
118}
119
120/// Fully validated whole-conversation participant state.
121///
122/// This state can execute restored edge authority. It has no public
123/// constructor and its fields are private: the sole producer is
124/// [`ParticipantConversationRestore::restore`], so possession of this value
125/// proves the complete snapshot validated jointly.
126///
127/// ```compile_fail
128/// use liminal_protocol::lifecycle::ParticipantConversationState;
129///
130/// fn fabricate() {
131///     let _ = ParticipantConversationState::<[u8; 4], [u8; 4], [u8; 4], [u8; 4]> {
132///         participants: unreachable!(),
133///         frontiers: unreachable!(),
134///         closure: unreachable!(),
135///     };
136/// }
137/// ```
138#[derive(Debug, PartialEq, Eq)]
139pub struct ParticipantConversationState<EF, V, LF, D> {
140    participants: Vec<RestoredParticipantLifecycle<EF, V, LF, D>>,
141    frontiers: ClaimFrontiers,
142    closure: ClosureState,
143}
144
145impl<EF, V, LF, D> ParticipantConversationState<EF, V, LF, D> {
146    /// Borrows every restored participant and tombstone.
147    #[must_use]
148    pub fn participants(&self) -> &[RestoredParticipantLifecycle<EF, V, LF, D>] {
149        &self.participants
150    }
151
152    /// Borrows the finalized claim frontiers.
153    #[must_use]
154    pub const fn frontiers(&self) -> &ClaimFrontiers {
155        &self.frontiers
156    }
157
158    /// Returns the exact closure state.
159    #[must_use]
160    pub const fn closure(&self) -> ClosureState {
161        self.closure
162    }
163
164    /// Consumes the validated state into its participant, frontier, and
165    /// closure components.
166    ///
167    /// Decomposition is one-way: no public path recombines components into
168    /// this validated form, so values split here cannot be spliced with
169    /// components restored from another snapshot.
170    #[must_use]
171    #[allow(clippy::type_complexity)]
172    pub fn into_parts(
173        self,
174    ) -> (
175        Vec<RestoredParticipantLifecycle<EF, V, LF, D>>,
176        ClaimFrontiers,
177        ClosureState,
178    ) {
179        (self.participants, self.frontiers, self.closure)
180    }
181}
182
183impl<EF: Clone, V: Clone, LF: Clone, D: Clone> ParticipantConversationState<EF, V, LF, D> {
184    /// Describes this validated state back into the description that
185    /// restores it, latch included.
186    ///
187    /// # Errors
188    ///
189    /// Returns [`ConversationDescribeError`] exactly as
190    /// [`describe_conversation`] does.
191    #[cfg(test)]
192    pub(super) fn describe(
193        &self,
194        retained_record_limit: u64,
195    ) -> Result<ParticipantConversationDescription<EF, V, LF, D>, ConversationDescribeError> {
196        let views: Vec<_> = self
197            .participants
198            .iter()
199            .map(RestoredParticipantLifecycle::view)
200            .collect();
201        describe_conversation(&views, &self.frontiers, retained_record_limit, self.closure)
202    }
203}
204
205impl RestoredConversationState {
206    /// Borrows the fully finalized coupled claim frontiers.
207    #[must_use]
208    #[cfg(test)]
209    pub(crate) const fn frontiers(&self) -> &ClaimFrontiers {
210        &self.frontiers
211    }
212
213    /// Returns the exact restored closure debt and stored edge.
214    #[must_use]
215    #[cfg(test)]
216    pub(crate) const fn closure(&self) -> ClosureState {
217        self.closure
218    }
219
220    /// Consumes the aggregate into the two values persisted by a server binding.
221    #[must_use]
222    pub(crate) fn into_parts(self) -> (ClaimFrontiers, ClosureState) {
223        (self.frontiers, self.closure)
224    }
225}
226
227/// Restores claim ownership and its marker-derived edge as one provenance unit.
228///
229/// Numeric/candidate validation runs first. At most one non-cloneable retained
230/// marker token is then consumed to restore the raw closure edge, after which
231/// frontier recovery claims are finalized against that exact typed edge.
232/// This standalone form intentionally rejects raw compacted causal history and
233/// ordinary-binding cursor authority; those require protocol-owned event replay
234/// to establish owned lifecycle provenance first. It is crate-private because
235/// accepting caller-authored snapshots here would upgrade inert storage bytes
236/// into executable edge authority.
237///
238/// # Errors
239///
240/// Returns [`ConversationStateRestoreError::ClaimFrontier`] for malformed
241/// numeric/candidate ownership or [`ConversationStateRestoreError::Storage`]
242/// for a missing, ambiguous, cross-conversation, or context-mismatched marker
243/// record and every other stored-edge provenance failure.
244#[cfg(test)]
245pub(super) fn restore_conversation_state(
246    frontier_restore: ClaimFrontiersRestore,
247    sequence_ledger: SequenceLedger,
248    order_ledger: OrderLedger,
249    closure_restore: &ClosureStateRestore,
250) -> Result<RestoredConversationState, ConversationStateRestoreError> {
251    let history = ValidatedConversationHistory::empty();
252    restore_conversation_with_history(
253        ClaimFrontiersDescription {
254            restore: frontier_restore,
255            sequence_ledger,
256            order_ledger,
257            fenced_marker_issued: None,
258        },
259        closure_restore,
260        &history,
261    )
262}
263
264/// Restores frontiers and closure from one frontier description.
265///
266/// The description's issuance latch is `None` on every public route; only the
267/// crate-private latch restore carries one, from a description that may have
268/// been decoded from untrusted capsule bytes. A carried `Some` is refused with
269/// [`StorageRestoreError::FencedMarkerLatch`] unless it names exactly the take
270/// the restored frontiers and closure edge would grant.
271fn restore_conversation_with_history(
272    frontier_description: ClaimFrontiersDescription,
273    closure_restore: &ClosureStateRestore,
274    history: &ValidatedConversationHistory,
275) -> Result<RestoredConversationState, ConversationStateRestoreError> {
276    let mut prevalidated = ClaimFrontiers::prevalidate_description(frontier_description, history)
277        .map_err(ConversationStateRestoreError::ClaimFrontier)?;
278    let marker_request = closure_restore.marker_record_request();
279    let ordinary_request = closure_restore.ordinary_binding_request();
280    let closure = match (marker_request, ordinary_request) {
281        (Some(marker_request), None) => {
282            let record = prevalidated.take_marker_record(marker_request).ok_or(
283                ConversationStateRestoreError::Storage(StorageRestoreError::StoredEdgeProvenance),
284            )?;
285            (*closure_restore)
286                .restore_with_marker_record(prevalidated.conversation_id(), record)
287                .map_err(ConversationStateRestoreError::Storage)?
288        }
289        (None, Some(binding)) => {
290            let origin = history
291                .ordinary_origin(
292                    binding.conversation_id,
293                    binding.participant_id,
294                    binding.binding_epoch,
295                )
296                .ok_or(ConversationStateRestoreError::Storage(
297                    StorageRestoreError::StoredEdgeProvenance,
298                ))?;
299            (*closure_restore)
300                .restore_with_binding_origin(origin)
301                .map_err(ConversationStateRestoreError::Storage)?
302        }
303        (None, None) => (*closure_restore)
304            .restore()
305            .map_err(ConversationStateRestoreError::Storage)?,
306        (Some(_), Some(_)) => {
307            return Err(ConversationStateRestoreError::Storage(
308                StorageRestoreError::StoredEdgeProvenance,
309            ));
310        }
311    };
312    let current_edge = match closure {
313        ClosureState::Clear => None,
314        ClosureState::Owed { edge, .. } => Some(edge),
315    };
316    let frontiers = prevalidated
317        .finish(current_edge)
318        .map_err(ConversationStateRestoreError::ClaimFrontier)?;
319    if !frontiers.fenced_latch_matches(current_edge) {
320        return Err(ConversationStateRestoreError::Storage(
321            StorageRestoreError::FencedMarkerLatch,
322        ));
323    }
324    Ok(RestoredConversationState { frontiers, closure })
325}
326
327/// Raw durable fields for one committed binding terminal.
328#[derive(Clone, Copy, Debug, PartialEq, Eq)]
329pub struct CommittedBindingTerminalRestore {
330    /// Binding authority that ended.
331    pub binding: ActiveBinding,
332    /// Cause stored with the terminal.
333    pub cause: CloseCause,
334    /// Immutable admission major.
335    pub transaction_order: TransactionOrder,
336    /// Durable lifecycle delivery sequence.
337    pub delivery_seq: DeliverySeq,
338}
339
340impl CommittedBindingTerminalRestore {
341    /// Validates and rebuilds the cause-partitioned committed terminal.
342    ///
343    /// # Errors
344    ///
345    /// Returns [`StorageRestoreError::CommittedBindingTerminal`] for an
346    /// impossible cause/suffix combination.
347    pub fn restore(self) -> Result<CommittedBindingTerminal, StorageRestoreError> {
348        restore_committed_terminal(
349            self.binding,
350            self.cause,
351            self.transaction_order,
352            self.delivery_seq,
353        )
354        .ok_or(StorageRestoreError::CommittedBindingTerminal)
355    }
356}
357
358/// Raw durable fields for one pending binding terminal.
359#[derive(Clone, Copy, Debug, PartialEq, Eq)]
360pub struct PendingFinalizationRestore {
361    /// Binding authority that already ended.
362    pub binding: ActiveBinding,
363    /// Cause stored with the pending terminal.
364    pub cause: CloseCause,
365    /// Immutable reserved admission major.
366    pub transaction_order: TransactionOrder,
367}
368
369impl PendingFinalizationRestore {
370    /// Validates and rebuilds the cause-partitioned pending terminal.
371    ///
372    /// # Errors
373    ///
374    /// Returns [`StorageRestoreError::PendingFinalization`] when the cause
375    /// cannot be pending or an unclean-restart suffix names another server.
376    pub fn restore(self) -> Result<PendingFinalization, StorageRestoreError> {
377        restore_pending_finalization(self.binding, self.cause, self.transaction_order)
378            .ok_or(StorageRestoreError::PendingFinalization)
379    }
380}
381
382/// Durable binding-fate terminal provenance, whether appended or still pending.
383#[derive(Clone, Copy, Debug, PartialEq, Eq)]
384pub enum BindingFateTerminalRestore {
385    /// Terminal appended in the binding-fate transaction.
386    Committed(CommittedBindingTerminalRestore),
387    /// Binding fate committed but its terminal append remains pending.
388    Pending(PendingFinalizationRestore),
389}
390
391/// Validated binding-fate terminal provenance.
392#[derive(Clone, Copy, Debug, PartialEq, Eq)]
393pub enum RestoredBindingFateTerminal {
394    /// Cause-partitioned committed terminal.
395    Committed(CommittedBindingTerminal),
396    /// Cause-partitioned pending terminal.
397    Pending(PendingFinalization),
398}
399
400impl BindingFateTerminalRestore {
401    /// Validates and rebuilds either durable terminal disposition.
402    ///
403    /// # Errors
404    ///
405    /// Returns [`StorageRestoreError`] for an impossible cause or restart suffix.
406    pub fn restore(self) -> Result<RestoredBindingFateTerminal, StorageRestoreError> {
407        match self {
408            Self::Committed(value) => value.restore().map(RestoredBindingFateTerminal::Committed),
409            Self::Pending(value) => value.restore().map(RestoredBindingFateTerminal::Pending),
410        }
411    }
412}
413
414impl RestoredBindingFateTerminal {
415    /// Builds the exact committed `Died` fate terminal a transition requires.
416    ///
417    /// Test-only. Production mints this by restoring a stored terminal or by
418    /// committing one through the binding's own terminal transition; this
419    /// shortcut exists so a fixture can satisfy the fate transition's authority
420    /// without rebuilding a whole conversation around it.
421    ///
422    /// # Panics
423    ///
424    /// Panics when the supplied fields do not form a valid committed terminal.
425    /// Builds the committed fate terminal that a marker-backed witness's own
426    /// fate requires, from that witness's own identity.
427    ///
428    /// Test-only, and it takes the witness rather than three loose scalars so a
429    /// fixture cannot quietly build a terminal for the wrong conversation: the
430    /// transition refuses a terminal that names another participant, epoch or
431    /// conversation, and a fixture that guessed would be refused for a reason
432    /// the test did not mean to exercise.
433    ///
434    /// # Panics
435    ///
436    /// Panics for a continuous witness, which has no conversation to name.
437    #[cfg(any(test, feature = "test-support"))]
438    #[allow(clippy::panic, clippy::expect_used)]
439    #[must_use]
440    pub fn committed_for_marker_progress(
441        progress: super::edge::ParticipantCursorProgress,
442        transaction_order: TransactionOrder,
443        delivery_seq: DeliverySeq,
444    ) -> Self {
445        let super::edge::ParticipantCursorProgress::Marker(marker) = progress else {
446            panic!("a continuous cursor witness names no conversation to build a terminal for")
447        };
448        Self::committed_for_test(
449            marker.conversation_id(),
450            marker.participant_id(),
451            marker.binding_epoch(),
452            transaction_order,
453            delivery_seq,
454        )
455    }
456
457    /// Builds the exact committed `Died` terminal an undelivered marker's own
458    /// pre-delivery fate must be given.
459    ///
460    /// Test-only, and derived from the delivery so a fixture cannot name a
461    /// conversation the delivery does not belong to.
462    #[cfg(any(test, feature = "test-support"))]
463    #[must_use]
464    pub fn committed_for_marker_delivery(
465        delivery: super::edge::MarkerDelivery,
466        transaction_order: TransactionOrder,
467        delivery_seq: DeliverySeq,
468    ) -> Self {
469        Self::committed_for_test(
470            delivery.conversation_id(),
471            delivery.participant_id(),
472            delivery.binding_epoch(),
473            transaction_order,
474            delivery_seq,
475        )
476    }
477
478    /// Builds an exact committed `Died` fate terminal from loose identity.
479    ///
480    /// Test-only. Prefer [`Self::committed_for_marker_progress`] or
481    /// [`Self::committed_for_marker_delivery`], which cannot name the wrong
482    /// conversation.
483    ///
484    /// # Panics
485    ///
486    /// Panics when the supplied fields do not form a valid committed terminal.
487    #[cfg(any(test, feature = "test-support"))]
488    #[allow(clippy::expect_used)]
489    #[must_use]
490    pub fn committed_for_test(
491        conversation_id: ConversationId,
492        participant_id: ParticipantId,
493        binding_epoch: BindingEpoch,
494        transaction_order: TransactionOrder,
495        delivery_seq: DeliverySeq,
496    ) -> Self {
497        BindingFateTerminalRestore::Committed(CommittedBindingTerminalRestore {
498            binding: ActiveBinding {
499                participant_id,
500                conversation_id,
501                binding_epoch,
502            },
503            cause: CloseCause::ConnectionLost,
504            transaction_order,
505            delivery_seq,
506        })
507        .restore()
508        .expect("test fate terminal fields form a valid committed terminal")
509    }
510
511    /// Describes this terminal as the capsule that restores it.
512    #[must_use]
513    pub const fn storage_description(self) -> BindingFateTerminalRestore {
514        match self {
515            Self::Committed(value) => {
516                BindingFateTerminalRestore::Committed(value.storage_description())
517            }
518            Self::Pending(value) => {
519                BindingFateTerminalRestore::Pending(value.storage_description())
520            }
521        }
522    }
523
524    pub(super) const fn participant_id(self) -> ParticipantId {
525        match self {
526            Self::Committed(value) => value.participant_id(),
527            Self::Pending(value) => value.participant_id(),
528        }
529    }
530
531    pub(super) const fn conversation_id(self) -> ConversationId {
532        match self {
533            Self::Committed(value) => value.conversation_id(),
534            Self::Pending(value) => value.conversation_id(),
535        }
536    }
537
538    pub(super) const fn binding_epoch(self) -> BindingEpoch {
539        match self {
540            Self::Committed(value) => value.binding_epoch(),
541            Self::Pending(value) => value.binding_epoch(),
542        }
543    }
544}
545
546/// Durable binding-slot representation with raw pending-terminal fields.
547#[derive(Clone, Copy, Debug, PartialEq, Eq)]
548pub enum BindingStateRestore {
549    /// No binding authority or pending terminal.
550    Detached,
551    /// Live binding authority.
552    Bound(ActiveBinding),
553    /// Ended authority whose terminal append remains pending.
554    PendingFinalization(PendingFinalizationRestore),
555}
556
557impl BindingStateRestore {
558    fn restore_for<EF>(self, member: &LiveMember<EF>) -> Result<BindingState, StorageRestoreError> {
559        let state = match self {
560            Self::Detached => BindingState::Detached,
561            Self::Bound(binding) => BindingState::Bound(binding),
562            Self::PendingFinalization(raw) => BindingState::PendingFinalization(raw.restore()?),
563        };
564        let authority_matches = match state {
565            BindingState::Detached => true,
566            BindingState::Bound(binding) => {
567                binding.participant_id == member.participant_id()
568                    && binding.conversation_id == member.conversation_id()
569                    && binding.binding_epoch.capability_generation == member.generation()
570            }
571            BindingState::PendingFinalization(pending) => {
572                pending.participant_id() == member.participant_id()
573                    && pending.conversation_id() == member.conversation_id()
574                    && pending.binding_epoch().capability_generation == member.generation()
575            }
576        };
577        if authority_matches {
578            Ok(state)
579        } else {
580            Err(StorageRestoreError::BindingAuthority)
581        }
582    }
583}
584
585/// Raw durable fields for the latest committed terminal retained by membership.
586#[derive(Clone, Debug, PartialEq, Eq)]
587pub struct LiveIdentityRestore<EF> {
588    /// Permanent participant identity/index.
589    pub participant_id: ParticipantId,
590    /// Owning conversation.
591    pub conversation_id: ConversationId,
592    /// Current credential generation.
593    pub generation: Generation,
594    /// Current attach secret.
595    pub attach_secret: AttachSecret,
596    /// Durable cumulative participant cursor.
597    pub cursor: DeliverySeq,
598    /// Permanent enrollment-token fingerprint.
599    pub enrollment_fingerprint: EnrollmentFingerprint<EF>,
600    /// Most recent committed binding terminal, if any.
601    pub latest_terminal: Option<CommittedBindingTerminalRestore>,
602}
603
604impl<EF> LiveIdentityRestore<EF> {
605    fn restore(self) -> Result<LiveMember<EF>, StorageRestoreError> {
606        let latest_terminal = self
607            .latest_terminal
608            .map(CommittedBindingTerminalRestore::restore)
609            .transpose()?;
610        LiveMember::restore(LiveMemberRestore {
611            participant_id: self.participant_id,
612            conversation_id: self.conversation_id,
613            generation: self.generation,
614            attach_secret: self.attach_secret,
615            cursor: self.cursor,
616            enrollment_fingerprint: self.enrollment_fingerprint,
617            latest_terminal,
618        })
619        .map_err(|_| StorageRestoreError::MembershipInvariant)
620    }
621}
622
623/// Raw fields of the canonical permanent `LeaveCommitted` result.
624#[derive(Clone, Copy, Debug, PartialEq, Eq)]
625pub struct LeaveCommittedRestore {
626    /// Conversation containing the participant.
627    pub conversation_id: ConversationId,
628    /// Committing Leave token.
629    pub leave_attempt_token: LeaveAttemptToken,
630    /// Retired participant.
631    pub participant_id: ParticipantId,
632    /// Permanent retired generation.
633    pub retired_generation: Generation,
634    /// Active binding ended in the Leave transaction, if any.
635    pub ended_binding_epoch: Option<BindingEpoch>,
636    /// Earlier binding-terminal delivery sequence, if any.
637    pub prior_terminal_delivery_seq: Option<DeliverySeq>,
638    /// Durable `Left` delivery sequence.
639    pub left_delivery_seq: DeliverySeq,
640}
641
642impl LeaveCommittedRestore {
643    /// Rebuilds the canonical terminal Leave result.
644    ///
645    /// # Errors
646    ///
647    /// Returns [`StorageRestoreError::LeaveResult`] for an epoch-generation
648    /// mismatch or a terminal sequence not strictly before `Left`.
649    pub fn restore(self) -> Result<LeaveCommitted, StorageRestoreError> {
650        LeaveCommitted::new(
651            self.conversation_id,
652            self.leave_attempt_token,
653            self.participant_id,
654            self.retired_generation,
655            self.ended_binding_epoch,
656            self.prior_terminal_delivery_seq,
657            self.left_delivery_seq,
658        )
659        .ok_or(StorageRestoreError::LeaveResult)
660    }
661}
662
663/// Complete raw durable tombstone fields.
664#[derive(Clone, Debug, PartialEq, Eq)]
665pub struct RetiredIdentityRestore<EF, V, LF> {
666    /// Permanent participant identity/index.
667    pub participant_id: ParticipantId,
668    /// Conversation containing the tombstone.
669    pub conversation_id: ConversationId,
670    /// Permanent retired generation.
671    pub retired_generation: Generation,
672    /// Permanent enrollment-token fingerprint.
673    pub enrollment_fingerprint: EnrollmentFingerprint<EF>,
674    /// Permanent committing Leave token.
675    pub leave_attempt_token: LeaveAttemptToken,
676    /// Stored non-reversible Leave-request verifier.
677    pub leave_request_verifier: V,
678    /// Stored canonical Leave fingerprint.
679    pub leave_fingerprint: LeaveFingerprint<LF>,
680    /// Immutable transaction-order major of the permanent `Left` record.
681    pub left_transaction_order: TransactionOrder,
682    /// Complete canonical committed result.
683    pub committed_result: LeaveCommittedRestore,
684}
685
686impl<EF, V, LF> RetiredIdentityRestore<EF, V, LF> {
687    fn restore(self) -> Result<RetiredIdentity<EF, V, LF>, StorageRestoreError> {
688        let result = self.committed_result.restore()?;
689        RetiredIdentity::restore(
690            self.participant_id,
691            self.conversation_id,
692            self.retired_generation,
693            self.enrollment_fingerprint,
694            self.leave_attempt_token,
695            self.leave_request_verifier,
696            self.leave_fingerprint,
697            self.left_transaction_order,
698            result,
699        )
700        .map_err(|_| StorageRestoreError::RetiredIdentity)
701    }
702}
703
704/// Exact four-state durable detach replay cell.
705#[derive(Clone, Debug, PartialEq, Eq)]
706pub enum DetachCellRestore<V> {
707    /// No detach replay state.
708    Empty,
709    /// Accepted detach whose terminal append remains pending.
710    Pending {
711        /// Stable attempt token.
712        token: DetachAttemptToken,
713        /// Cell owner.
714        participant_id: ParticipantId,
715        /// Request generation.
716        request_generation: Generation,
717        /// Non-reversible exact-request verifier.
718        request_verifier: V,
719        /// Binding epoch ended by detach.
720        committed_binding_epoch: BindingEpoch,
721        /// Immutable binding-terminal admission position.
722        admission_order: AdmissionOrder,
723        /// Observer refusal epoch.
724        refused_epoch: ObserverEpoch,
725    },
726    /// Committed detach retaining its exact terminal sequence.
727    Committed {
728        /// Stable attempt token.
729        token: DetachAttemptToken,
730        /// Cell owner.
731        participant_id: ParticipantId,
732        /// Request generation.
733        request_generation: Generation,
734        /// Non-reversible exact-request verifier.
735        request_verifier: V,
736        /// Binding epoch ended by detach.
737        committed_binding_epoch: BindingEpoch,
738        /// Committed `Detached` delivery sequence.
739        detached_delivery_seq: DeliverySeq,
740    },
741    /// Post-attach replay state retaining the old binding epoch.
742    Terminalized {
743        /// Stable old detach token.
744        token: DetachAttemptToken,
745        /// Cell owner.
746        participant_id: ParticipantId,
747        /// Old request generation.
748        request_generation: Generation,
749        /// Non-reversible exact-request verifier.
750        request_verifier: V,
751        /// Old binding epoch ended by detach.
752        committed_binding_epoch: BindingEpoch,
753    },
754}
755
756impl<V> DetachCellRestore<V> {
757    fn restore(self) -> Result<DetachCell<V>, StorageRestoreError> {
758        match self {
759            Self::Empty => Ok(DetachCell::Empty(EmptyDetach)),
760            Self::Pending {
761                token,
762                participant_id,
763                request_generation,
764                request_verifier,
765                committed_binding_epoch,
766                admission_order,
767                refused_epoch,
768            } => restore_pending_detach(
769                token,
770                participant_id,
771                request_generation,
772                request_verifier,
773                committed_binding_epoch,
774                admission_order,
775                refused_epoch,
776            )
777            .map(DetachCell::Pending)
778            .ok_or(StorageRestoreError::DetachCell),
779            Self::Committed {
780                token,
781                participant_id,
782                request_generation,
783                request_verifier,
784                committed_binding_epoch,
785                detached_delivery_seq,
786            } => restore_committed_detach(
787                token,
788                participant_id,
789                request_generation,
790                request_verifier,
791                committed_binding_epoch,
792                detached_delivery_seq,
793            )
794            .map(DetachCell::Committed)
795            .ok_or(StorageRestoreError::DetachCell),
796            Self::Terminalized {
797                token,
798                participant_id,
799                request_generation,
800                request_verifier,
801                committed_binding_epoch,
802            } => restore_terminalized_detach(
803                token,
804                participant_id,
805                request_generation,
806                request_verifier,
807                committed_binding_epoch,
808            )
809            .map(DetachCell::Terminalized)
810            .ok_or(StorageRestoreError::DetachCell),
811        }
812    }
813}
814
815/// Complete event-replayed participant state, with tombstone precedence in the type.
816#[allow(
817    clippy::large_enum_variant,
818    reason = "the live storage capsule remains inline so its atomic slots cannot be restored separately"
819)]
820#[derive(Clone, Debug, PartialEq, Eq)]
821pub enum ParticipantLifecycleRestore<EF, V, LF, D> {
822    /// Live identity and its atomically paired binding and detach slots.
823    Live {
824        /// Membership and terminal history.
825        identity: LiveIdentityRestore<EF>,
826        /// Binding slot.
827        binding: BindingStateRestore,
828        /// Current or last binding's producer-emitted origin capsule.
829        binding_origin: Option<BindingOrigin>,
830        /// Four-state detach replay cell.
831        detach_cell: DetachCellRestore<D>,
832    },
833    /// Permanent tombstone; no live binding or detach slot can accompany it.
834    Retired(RetiredIdentityRestore<EF, V, LF>),
835}
836
837/// Validated runtime participant state restored from durable data.
838#[allow(
839    clippy::large_enum_variant,
840    reason = "the validated live capsule remains inline as one atomic lifecycle result"
841)]
842#[derive(Clone, Debug, PartialEq, Eq)]
843pub enum RestoredParticipantLifecycle<EF, V, LF, D> {
844    /// Valid live membership and its paired slots.
845    Live {
846        /// Validated live member.
847        member: LiveMember<EF>,
848        /// Validated binding state.
849        binding: BindingState,
850        /// Validated current or last binding origin, when a binding has existed.
851        binding_origin: Option<BindingOrigin>,
852        /// Validated detach cell.
853        detach_cell: DetachCell<D>,
854    },
855    /// Permanent retired identity with no remaining live slots.
856    Retired(RetiredIdentity<EF, V, LF>),
857}
858
859impl<EF, V, LF, D> ParticipantLifecycleRestore<EF, V, LF, D> {
860    /// Validates one complete atomic participant snapshot.
861    ///
862    /// # Errors
863    ///
864    /// Returns [`StorageRestoreError`] when any raw state is invalid or the
865    /// membership, binding, terminal history, and detach-cell variants disagree.
866    pub fn restore(
867        self,
868    ) -> Result<RestoredParticipantLifecycle<EF, V, LF, D>, StorageRestoreError> {
869        match self {
870            Self::Retired(identity) => identity
871                .restore()
872                .map(RestoredParticipantLifecycle::Retired),
873            Self::Live {
874                identity,
875                binding,
876                binding_origin,
877                detach_cell,
878            } => {
879                let member = identity.restore()?;
880                let binding = binding.restore_for(&member)?;
881                let binding_origin = binding_origin
882                    .map(|origin| validate_binding_origin(origin, &member, binding))
883                    .transpose()?;
884                let origin_required = !matches!(binding, BindingState::Detached)
885                    || member.latest_terminal().is_some();
886                if origin_required != binding_origin.is_some() {
887                    return Err(StorageRestoreError::BindingAuthority);
888                }
889                let detach_cell = detach_cell.restore()?;
890                validate_live_pair(&member, binding, &detach_cell)?;
891                Ok(RestoredParticipantLifecycle::Live {
892                    member,
893                    binding,
894                    binding_origin,
895                    detach_cell,
896                })
897            }
898        }
899    }
900}
901
902fn validate_binding_origin<EF>(
903    origin: BindingOrigin,
904    member: &LiveMember<EF>,
905    binding_state: BindingState,
906) -> Result<BindingOrigin, StorageRestoreError> {
907    let expected_epoch = match binding_state {
908        BindingState::Bound(current) => Some(current.binding_epoch),
909        BindingState::PendingFinalization(pending) => Some(pending.binding_epoch()),
910        BindingState::Detached => member
911            .latest_terminal()
912            .map(CommittedBindingTerminal::binding_epoch),
913    };
914    let attached = origin.attached();
915    if origin.participant_id() != member.participant_id()
916        || origin.conversation_id() != member.conversation_id()
917        || expected_epoch != Some(origin.binding_epoch())
918        || attached.participant_id() != member.participant_id()
919        || attached.conversation_id() != member.conversation_id()
920    {
921        Err(StorageRestoreError::BindingAuthority)
922    } else {
923        Ok(origin)
924    }
925}
926
927impl<EF, V, LF, D> RestoredParticipantLifecycle<EF, V, LF, D> {
928    /// Consumes the restored snapshot into the crate's identity and optional live slots.
929    #[must_use]
930    #[allow(clippy::type_complexity)]
931    pub fn into_parts(
932        self,
933    ) -> (
934        IdentityState<EF, V, LF>,
935        Option<BindingState>,
936        Option<DetachCell<D>>,
937    ) {
938        match self {
939            Self::Live {
940                member,
941                binding,
942                binding_origin: _,
943                detach_cell,
944            } => (
945                IdentityState::Live(member),
946                Some(binding),
947                Some(detach_cell),
948            ),
949            Self::Retired(identity) => (IdentityState::Retired(identity), None, None),
950        }
951    }
952}
953
954impl<EF, V, LF, D> ParticipantConversationRestore<EF, V, LF, D> {
955    /// Restores participant lifecycle, sealed history/origins, frontiers, and
956    /// closure as one total conversation snapshot.
957    ///
958    /// The conversation history backing frontier and closure validation is
959    /// derived exclusively from the participants restored in this same call —
960    /// never from an empty placeholder — so raw causal rows, binding origins,
961    /// and marker ownership must be proven by the exact membership and
962    /// tombstone capsules supplied alongside them.
963    ///
964    /// # Errors
965    ///
966    /// Returns a storage error when participant snapshots or binding origins
967    /// disagree, and a claim-frontier error when the exact participant-derived
968    /// history does not back raw causal rows and marker ownership.
969    pub fn restore(
970        self,
971    ) -> Result<ParticipantConversationState<EF, V, LF, D>, ConversationStateRestoreError> {
972        self.restore_with_fenced_latch(None)
973    }
974
975    /// Restores exactly as [`Self::restore`] does, then installs the
976    /// fenced-marker issuance latch a description carried, once it is checked.
977    ///
978    /// The latch is not part of the public restore input, but it reaches this
979    /// entry from capsule bytes the public decoder accepts, so it is checked,
980    /// not trusted. Every validation the public route runs runs here, in the
981    /// same order, on the same input; then a `Some` latch must name exactly the
982    /// take the restored state would grant: the closure edge is that
983    /// participant's detached credential recovery of that marker on that prior
984    /// epoch, and the marker occurrence is still active for the participant
985    /// detached on that epoch. A `None` cannot be checked, because a take
986    /// writes nothing but the latch.
987    ///
988    /// # Errors
989    ///
990    /// Returns the errors [`Self::restore`] returns, and
991    /// [`StorageRestoreError::FencedMarkerLatch`] for a latch no live take
992    /// could have set.
993    pub(in crate::lifecycle) fn restore_with_fenced_latch(
994        self,
995        fenced_marker_issued: Option<(ParticipantId, DeliverySeq, BindingEpoch)>,
996    ) -> Result<ParticipantConversationState<EF, V, LF, D>, ConversationStateRestoreError> {
997        let participants = self
998            .participants
999            .into_iter()
1000            .map(ParticipantLifecycleRestore::restore)
1001            .collect::<Result<Vec<_>, _>>()
1002            .map_err(ConversationStateRestoreError::Storage)?;
1003        validate_participant_frontier_projection(
1004            &participants,
1005            self.frontiers.conversation_id,
1006            &self.frontiers.active_identities,
1007        )?;
1008        let history = validated_conversation_history(&participants)?;
1009        let restored = restore_conversation_with_history(
1010            ClaimFrontiersDescription {
1011                restore: self.frontiers,
1012                sequence_ledger: self.sequence_ledger,
1013                order_ledger: self.order_ledger,
1014                fenced_marker_issued,
1015            },
1016            &self.closure,
1017            &history,
1018        )?;
1019        let (frontiers, closure) = restored.into_parts();
1020        Ok(ParticipantConversationState {
1021            participants,
1022            frontiers,
1023            closure,
1024        })
1025    }
1026}
1027
1028/// One participant's live state, lent by reference to be described.
1029///
1030/// It holds references only, so a binding can lend the values it keeps but
1031/// cannot make one: every referenced value is itself produced only by the
1032/// protocol's own transitions or restores.
1033#[derive(Debug)]
1034pub enum ParticipantLifecycleView<'a, EF, V, LF, D> {
1035    /// A live member with its atomically paired slots.
1036    Live {
1037        /// Membership and terminal history.
1038        member: &'a LiveMember<EF>,
1039        /// Binding slot.
1040        binding: &'a BindingState,
1041        /// Current or last binding's producer-emitted origin, when a binding
1042        /// has existed.
1043        binding_origin: Option<&'a BindingOrigin>,
1044        /// Four-state detach replay cell.
1045        detach_cell: &'a DetachCell<D>,
1046    },
1047    /// A permanent tombstone.
1048    Retired(&'a RetiredIdentity<EF, V, LF>),
1049}
1050
1051impl<EF, V, LF, D> ParticipantLifecycleView<'_, EF, V, LF, D> {
1052    /// The permanent participant id the lent state belongs to.
1053    #[must_use]
1054    pub const fn participant_id(&self) -> ParticipantId {
1055        match self {
1056            Self::Live { member, .. } => member.participant_id(),
1057            Self::Retired(retired) => retired.participant_id(),
1058        }
1059    }
1060}
1061
1062impl<EF: Clone, V: Clone, LF: Clone, D: Clone> ParticipantLifecycleView<'_, EF, V, LF, D> {
1063    /// Describes the lent participant as the raw capsule that restores it,
1064    /// field by field. Generic payloads are cloned; nothing is moved.
1065    pub(super) fn storage_description(&self) -> ParticipantLifecycleRestore<EF, V, LF, D> {
1066        match *self {
1067            Self::Live {
1068                member,
1069                binding,
1070                binding_origin,
1071                detach_cell,
1072            } => ParticipantLifecycleRestore::Live {
1073                identity: LiveIdentityRestore {
1074                    participant_id: member.participant_id(),
1075                    conversation_id: member.conversation_id(),
1076                    generation: member.generation(),
1077                    attach_secret: member.attach_secret(),
1078                    cursor: member.cursor(),
1079                    enrollment_fingerprint: member.enrollment_fingerprint().clone(),
1080                    latest_terminal: member
1081                        .latest_terminal()
1082                        .map(CommittedBindingTerminal::storage_description),
1083                },
1084                binding: describe_binding_state(*binding),
1085                binding_origin: binding_origin.copied(),
1086                detach_cell: detach_cell.storage_description(),
1087            },
1088            Self::Retired(retired) => {
1089                ParticipantLifecycleRestore::Retired(describe_retired(retired))
1090            }
1091        }
1092    }
1093}
1094
1095impl<EF, V, LF, D> RestoredParticipantLifecycle<EF, V, LF, D> {
1096    /// Lends this restored participant as a view to describe it.
1097    #[must_use]
1098    pub const fn view(&self) -> ParticipantLifecycleView<'_, EF, V, LF, D> {
1099        match self {
1100            Self::Live {
1101                member,
1102                binding,
1103                binding_origin,
1104                detach_cell,
1105            } => ParticipantLifecycleView::Live {
1106                member,
1107                binding,
1108                binding_origin: binding_origin.as_ref(),
1109                detach_cell,
1110            },
1111            Self::Retired(retired) => ParticipantLifecycleView::Retired(retired),
1112        }
1113    }
1114}
1115
1116const fn describe_binding_state(binding: BindingState) -> BindingStateRestore {
1117    match binding {
1118        BindingState::Detached => BindingStateRestore::Detached,
1119        BindingState::Bound(binding) => BindingStateRestore::Bound(binding),
1120        BindingState::PendingFinalization(pending) => {
1121            BindingStateRestore::PendingFinalization(pending.storage_description())
1122        }
1123    }
1124}
1125
1126fn describe_retired<EF: Clone, V: Clone, LF: Clone>(
1127    retired: &RetiredIdentity<EF, V, LF>,
1128) -> RetiredIdentityRestore<EF, V, LF> {
1129    let result = retired.committed_result();
1130    RetiredIdentityRestore {
1131        participant_id: retired.participant_id(),
1132        conversation_id: retired.conversation_id(),
1133        retired_generation: retired.retired_generation(),
1134        enrollment_fingerprint: retired.enrollment_fingerprint().clone(),
1135        leave_attempt_token: retired.leave_attempt_token(),
1136        leave_request_verifier: retired.leave_request_verifier().clone(),
1137        leave_fingerprint: retired.leave_fingerprint().clone(),
1138        left_transaction_order: retired.left_admission_order().transaction_order(),
1139        committed_result: LeaveCommittedRestore {
1140            conversation_id: result.conversation_id(),
1141            leave_attempt_token: result.leave_attempt_token(),
1142            participant_id: result.participant_id(),
1143            retired_generation: result.retired_generation(),
1144            ended_binding_epoch: result.ended_binding_epoch(),
1145            prior_terminal_delivery_seq: result.prior_terminal_delivery_seq(),
1146            left_delivery_seq: result.left_delivery_seq(),
1147        },
1148    }
1149}
1150
1151/// A live conversation that cannot be described back into the capsule that
1152/// would restore it.
1153#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1154#[non_exhaustive]
1155pub enum ConversationDescribeError {
1156    /// The current edge is a detached cursor release. Its live value keeps
1157    /// only the participant and the dead epoch and drops the ordinary or
1158    /// recovered fate provenance its capsule must carry, so no description
1159    /// of it can be produced from live state.
1160    DetachedCursorReleaseProvenance,
1161    /// A detached recovery or release edge needs the terminal of the fate that
1162    /// selected it, and no lent live participant holds a terminal for that
1163    /// participant and epoch.
1164    EdgeTerminal,
1165}
1166
1167/// Crate-private description of one whole live conversation: the public
1168/// restore input, plus the fenced-marker issuance latch that input
1169/// deliberately cannot name.
1170#[derive(Clone, Debug, PartialEq, Eq)]
1171pub(super) struct ParticipantConversationDescription<EF, V, LF, D> {
1172    /// The public restore input for every participant, both frontiers and the
1173    /// closure.
1174    pub(super) restore: ParticipantConversationRestore<EF, V, LF, D>,
1175    /// The fenced-marker issuance latch, carried beside the input.
1176    pub(super) fenced_marker_issued: Option<(ParticipantId, DeliverySeq, BindingEpoch)>,
1177}
1178
1179impl<EF, V, LF, D> ParticipantConversationDescription<EF, V, LF, D> {
1180    /// Restores the description through the one joint validation, then
1181    /// installs its latch.
1182    ///
1183    /// # Errors
1184    ///
1185    /// Returns the errors [`ParticipantConversationRestore::restore`] returns,
1186    /// and [`StorageRestoreError::FencedMarkerLatch`] for a carried latch no
1187    /// live take could have set.
1188    pub(super) fn into_state(
1189        self,
1190    ) -> Result<ParticipantConversationState<EF, V, LF, D>, ConversationStateRestoreError> {
1191        self.restore
1192            .restore_with_fenced_latch(self.fenced_marker_issued)
1193    }
1194}
1195
1196/// Describes one whole live conversation from borrowed live values.
1197///
1198/// Frontiers go through [`ClaimFrontiers::describe`], participants through
1199/// their views, and the closure through each edge's own storage description.
1200/// A detached recovery or release edge takes the terminal of its selecting
1201/// fate from the lent member it belongs to (or its pending finalization),
1202/// which is sound because a standing detached edge refuses ordinary detached
1203/// attach, so the member cannot have committed a later terminal.
1204///
1205/// # Errors
1206///
1207/// Returns [`ConversationDescribeError`] when the closure cannot be described:
1208/// a detached cursor release, or a detached edge whose terminal no lent
1209/// participant holds.
1210pub(super) fn describe_conversation<EF: Clone, V: Clone, LF: Clone, D: Clone>(
1211    participants: &[ParticipantLifecycleView<'_, EF, V, LF, D>],
1212    frontiers: &ClaimFrontiers,
1213    retained_record_limit: u64,
1214    closure: ClosureState,
1215) -> Result<ParticipantConversationDescription<EF, V, LF, D>, ConversationDescribeError> {
1216    let closure = describe_closure(participants, frontiers.conversation_id(), closure)?;
1217    let ClaimFrontiersDescription {
1218        restore,
1219        sequence_ledger,
1220        order_ledger,
1221        fenced_marker_issued,
1222    } = frontiers.describe(retained_record_limit);
1223    Ok(ParticipantConversationDescription {
1224        restore: ParticipantConversationRestore {
1225            participants: participants
1226                .iter()
1227                .map(ParticipantLifecycleView::storage_description)
1228                .collect(),
1229            frontiers: restore,
1230            sequence_ledger,
1231            order_ledger,
1232            closure,
1233        },
1234        fenced_marker_issued,
1235    })
1236}
1237
1238fn describe_closure<EF, V, LF, D>(
1239    participants: &[ParticipantLifecycleView<'_, EF, V, LF, D>],
1240    conversation_id: ConversationId,
1241    closure: ClosureState,
1242) -> Result<ClosureStateRestore, ConversationDescribeError> {
1243    let ClosureState::Owed { debt, edge } = closure else {
1244        return Ok(ClosureStateRestore::Clear);
1245    };
1246    let edge = match edge {
1247        StoredEdge::ObserverProjection(edge) => edge.storage_description(),
1248        StoredEdge::PhysicalCompaction(edge) => edge.storage_description(),
1249        StoredEdge::MarkerDelivery(edge) => edge.storage_description(),
1250        StoredEdge::ParticipantCursorProgress(ParticipantCursorProgress::Continuous(edge)) => {
1251            edge.storage_description(conversation_id)
1252        }
1253        StoredEdge::ParticipantCursorProgress(ParticipantCursorProgress::Marker(edge)) => {
1254            edge.storage_description()
1255        }
1256        StoredEdge::DetachedCredentialRecovery(edge) => {
1257            let terminal = selecting_fate_terminal(
1258                participants,
1259                edge.participant_id(),
1260                edge.prior_binding_epoch(),
1261            )
1262            .ok_or(ConversationDescribeError::EdgeTerminal)?;
1263            edge.storage_description(terminal)
1264                .map_err(|_| ConversationDescribeError::EdgeTerminal)?
1265        }
1266        StoredEdge::DetachedMarkerRelease(edge) => {
1267            let terminal = match edge.provenance() {
1268                DetachedMarkerReleaseProvenance::Fate => Some(
1269                    selecting_fate_terminal(
1270                        participants,
1271                        edge.participant_id(),
1272                        edge.last_dead_binding_epoch(),
1273                    )
1274                    .ok_or(ConversationDescribeError::EdgeTerminal)?,
1275                ),
1276                DetachedMarkerReleaseProvenance::Candidate => None,
1277            };
1278            edge.storage_description(terminal)
1279                .map_err(|_| ConversationDescribeError::EdgeTerminal)?
1280        }
1281        StoredEdge::DetachedCursorRelease(_) => {
1282            return Err(ConversationDescribeError::DetachedCursorReleaseProvenance);
1283        }
1284    };
1285    Ok(ClosureStateRestore::Owed {
1286        debt: debt.value(),
1287        edge,
1288    })
1289}
1290
1291/// The terminal of the fate that ended `binding_epoch` for `participant_id`:
1292/// a pending finalization still in the binding slot, else the member's latest
1293/// committed terminal, provided it names that exact epoch.
1294fn selecting_fate_terminal<EF, V, LF, D>(
1295    participants: &[ParticipantLifecycleView<'_, EF, V, LF, D>],
1296    participant_id: ParticipantId,
1297    binding_epoch: BindingEpoch,
1298) -> Option<RestoredBindingFateTerminal> {
1299    participants.iter().find_map(|participant| {
1300        let ParticipantLifecycleView::Live {
1301            member, binding, ..
1302        } = participant
1303        else {
1304            return None;
1305        };
1306        if member.participant_id() != participant_id {
1307            return None;
1308        }
1309        if let BindingState::PendingFinalization(pending) = **binding
1310            && pending.binding_epoch() == binding_epoch
1311        {
1312            return Some(RestoredBindingFateTerminal::Pending(pending));
1313        }
1314        member
1315            .latest_terminal()
1316            .filter(|terminal| terminal.binding_epoch() == binding_epoch)
1317            .map(RestoredBindingFateTerminal::Committed)
1318    })
1319}
1320
1321fn validated_conversation_history<EF, V, LF, D>(
1322    participants: &[RestoredParticipantLifecycle<EF, V, LF, D>],
1323) -> Result<ValidatedConversationHistory, ConversationStateRestoreError> {
1324    let mut causal_authorities = Vec::new();
1325    let mut binding_origins = Vec::new();
1326    let mut seen = Vec::with_capacity(participants.len());
1327    for participant in participants {
1328        let participant_id = match participant {
1329            RestoredParticipantLifecycle::Live {
1330                member,
1331                binding_origin,
1332                ..
1333            } => {
1334                if let Some(terminal) = member.latest_terminal() {
1335                    causal_authorities
1336                        .push(HistoricalCausalAuthority::from_committed_terminal(terminal));
1337                }
1338                if let Some(origin) = binding_origin {
1339                    binding_origins.push(*origin);
1340                }
1341                member.participant_id()
1342            }
1343            RestoredParticipantLifecycle::Retired(retired) => {
1344                causal_authorities.push(HistoricalCausalAuthority::from_retired(retired));
1345                retired.participant_id()
1346            }
1347        };
1348        seen.push(participant_id);
1349    }
1350    // Conversations carry no participant-count cap, so the duplicate check is
1351    // sort-and-scan (O(n log n)) rather than a per-participant linear scan
1352    // (O(n^2)) over the whole-conversation cold-restore path.
1353    seen.sort_unstable();
1354    let has_duplicate = seen
1355        .iter()
1356        .zip(seen.iter().skip(1))
1357        .any(|(current, next)| current == next);
1358    if has_duplicate {
1359        return Err(ConversationStateRestoreError::Storage(
1360            StorageRestoreError::MembershipInvariant,
1361        ));
1362    }
1363    Ok(ValidatedConversationHistory::new(
1364        causal_authorities,
1365        binding_origins,
1366    ))
1367}
1368
1369fn validate_participant_frontier_projection<EF, V, LF, D>(
1370    participants: &[RestoredParticipantLifecycle<EF, V, LF, D>],
1371    conversation_id: ConversationId,
1372    frontier: &[super::FrontierParticipant],
1373) -> Result<(), ConversationStateRestoreError> {
1374    let mut projected = Vec::new();
1375    for participant in participants {
1376        match participant {
1377            RestoredParticipantLifecycle::Live {
1378                member, binding, ..
1379            } => {
1380                if member.conversation_id() != conversation_id {
1381                    return Err(ConversationStateRestoreError::Storage(
1382                        StorageRestoreError::MembershipInvariant,
1383                    ));
1384                }
1385                let binding = match binding {
1386                    BindingState::Bound(binding) => FrontierBinding::Bound(binding.binding_epoch),
1387                    BindingState::PendingFinalization(pending) => {
1388                        FrontierBinding::Detached(pending.binding_epoch())
1389                    }
1390                    BindingState::Detached => {
1391                        let Some(terminal) = member.latest_terminal() else {
1392                            return Err(ConversationStateRestoreError::Storage(
1393                                StorageRestoreError::BindingAuthority,
1394                            ));
1395                        };
1396                        FrontierBinding::Detached(terminal.binding_epoch())
1397                    }
1398                };
1399                projected.push(super::FrontierParticipant::new(
1400                    member.participant_id(),
1401                    member.cursor(),
1402                    binding,
1403                ));
1404            }
1405            RestoredParticipantLifecycle::Retired(retired) => {
1406                if retired.conversation_id() != conversation_id {
1407                    return Err(ConversationStateRestoreError::Storage(
1408                        StorageRestoreError::MembershipInvariant,
1409                    ));
1410                }
1411            }
1412        }
1413    }
1414    projected.sort_by_key(|participant| participant.participant_index());
1415    if projected == frontier {
1416        Ok(())
1417    } else {
1418        Err(ConversationStateRestoreError::Storage(
1419            StorageRestoreError::MembershipInvariant,
1420        ))
1421    }
1422}
1423
1424#[allow(clippy::suspicious_operation_groupings)]
1425fn validate_live_pair<EF, D>(
1426    member: &LiveMember<EF>,
1427    binding: BindingState,
1428    detach_cell: &DetachCell<D>,
1429) -> Result<(), StorageRestoreError> {
1430    match detach_cell {
1431        DetachCell::Empty(_) => Ok(()),
1432        DetachCell::Pending(cell) => {
1433            if cell.participant_id() != member.participant_id()
1434                || cell.request_generation() != member.generation()
1435                || validate_pending_pair(binding, cell, Some(member.conversation_id())).is_err()
1436            {
1437                Err(StorageRestoreError::DetachBindingPair)
1438            } else {
1439                Ok(())
1440            }
1441        }
1442        DetachCell::Committed(cell) => {
1443            let terminal_matches = member.latest_terminal().is_some_and(|terminal| {
1444                terminal.participant_id() == cell.participant_id()
1445                    && terminal.conversation_id() == member.conversation_id()
1446                    && terminal.binding_epoch() == cell.committed_binding_epoch()
1447                    && terminal.delivery_seq() == cell.detached_delivery_seq()
1448                    && terminal.detached_cause()
1449                        == Some(crate::wire::DetachedCause::CleanDeregister)
1450            });
1451            if cell.participant_id() != member.participant_id()
1452                || cell.request_generation() != member.generation()
1453                || binding != BindingState::Detached
1454                || !terminal_matches
1455            {
1456                Err(StorageRestoreError::DetachBindingPair)
1457            } else {
1458                Ok(())
1459            }
1460        }
1461        DetachCell::Terminalized(cell) => {
1462            if cell.participant_id() != member.participant_id()
1463                || cell.request_generation().get() >= member.generation().get()
1464            {
1465                Err(StorageRestoreError::DetachBindingPair)
1466            } else {
1467                Ok(())
1468            }
1469        }
1470    }
1471}
1472
1473/// Complete raw state for one participant-scoped nonzero-debt cursor episode.
1474#[derive(Clone, Debug, PartialEq, Eq)]
1475pub struct CursorEpisodeRestore {
1476    /// Owning conversation.
1477    pub conversation_id: ConversationId,
1478    /// Raw componentwise closure debt, validated nonzero during restoration.
1479    pub debt: WideResourceVector,
1480    /// Durable hard-observer progress `o`.
1481    pub observer_progress: DeliverySeq,
1482    /// Candidate high watermark `H'`.
1483    pub candidate_high_watermark: DeliverySeq,
1484    /// Current durable floor `F`.
1485    pub current_floor: u128,
1486    /// Current append-free class capacity floor.
1487    pub cap_floor: u128,
1488    /// Bound participant cursors keyed by their embedded permanent ids.
1489    pub participants: Vec<BoundParticipantCursor>,
1490    /// Variable facts keyed by `(participant_index, boundary)`.
1491    pub facts: Vec<(CursorProgressKey, CursorProgressFact)>,
1492}
1493
1494impl CursorEpisodeRestore {
1495    /// Validates and rebuilds one cursor episode and all variable facts.
1496    ///
1497    /// # Errors
1498    ///
1499    /// Returns [`StorageRestoreError::CursorEpisode`] for an invalid floor,
1500    /// duplicate/unknown participant, duplicate fact, out-of-range boundary, or
1501    /// fact state inconsistent with the participant cursor.
1502    pub fn restore(self) -> Result<NonzeroDebtCursorEpisode, StorageRestoreError> {
1503        let debt = ClosureDebt::new(self.debt).ok_or(StorageRestoreError::ClosureDebt)?;
1504        NonzeroDebtCursorEpisode::restore(
1505            self.conversation_id,
1506            debt,
1507            self.observer_progress,
1508            self.candidate_high_watermark,
1509            self.current_floor,
1510            self.cap_floor,
1511            self.participants,
1512            self.facts,
1513        )
1514        .ok_or(StorageRestoreError::CursorEpisode)
1515    }
1516}
1517
1518/// Raw predecessor fields for an ordinary-attach binding authority.
1519///
1520/// Raw fields are not executable provenance. Standalone restoration is absent:
1521/// total participant-conversation restore must first prove an exact unfenced
1522/// binding-origin capsule.
1523///
1524/// ```compile_fail
1525/// use liminal_protocol::lifecycle::OrdinaryBindingAuthorityRestore;
1526///
1527/// fn bypass(raw: OrdinaryBindingAuthorityRestore) {
1528///     let _ = raw.restore();
1529/// }
1530/// ```
1531#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1532pub struct OrdinaryBindingAuthorityRestore {
1533    /// Exact binding installed by ordinary attach.
1534    pub binding: ActiveBinding,
1535    /// Durable no-marker cursor carried through the attach.
1536    pub through_seq: DeliverySeq,
1537}
1538
1539impl OrdinaryBindingAuthorityRestore {
1540    fn restore_with_origin(
1541        self,
1542        origin: &BindingOrigin,
1543    ) -> Result<OrdinaryBindingAuthority, StorageRestoreError> {
1544        if !origin.is_unfenced()
1545            || origin.conversation_id() != self.binding.conversation_id
1546            || origin.participant_id() != self.binding.participant_id
1547            || origin.binding_epoch() != self.binding.binding_epoch
1548        {
1549            return Err(StorageRestoreError::StoredEdgeProvenance);
1550        }
1551        Ok(OrdinaryBindingAuthority::new(
1552            self.binding,
1553            self.through_seq,
1554        ))
1555    }
1556}
1557
1558/// Raw predecessor fields proving exact marker delivery.
1559///
1560/// A retained-record authority is mandatory and cannot be constructed by a
1561/// storage binding. Raw edge fields alone therefore fail at compile time.
1562///
1563/// ```compile_fail
1564/// use liminal_protocol::{
1565///     lifecycle::MarkerDeliveryRestore,
1566///     wire::BindingEpoch,
1567/// };
1568///
1569/// fn raw_restore(epoch: BindingEpoch) {
1570///     let raw = MarkerDeliveryRestore {
1571///         participant_id: 7,
1572///         binding_epoch: epoch,
1573///         marker_delivery_seq: 11,
1574///     };
1575///     let _ = raw.restore_bound(1);
1576/// }
1577/// ```
1578#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1579pub struct MarkerDeliveryRestore {
1580    /// Participant that received the marker.
1581    pub participant_id: ParticipantId,
1582    /// Exact receiving binding epoch.
1583    pub binding_epoch: BindingEpoch,
1584    /// Exact delivered marker sequence.
1585    pub marker_delivery_seq: DeliverySeq,
1586}
1587
1588impl MarkerDeliveryRestore {
1589    /// Rebuilds a live marker-delivery witness after exact record-field matching.
1590    ///
1591    /// # Errors
1592    ///
1593    /// Returns [`StorageRestoreError::StoredEdgeProvenance`] when the authority
1594    /// belongs to another conversation, names a detached target, or disagrees
1595    /// with any raw edge field.
1596    #[cfg(any(test, feature = "test-support"))]
1597    pub(super) fn restore_bound(
1598        self,
1599        conversation_id: ConversationId,
1600        record_authority: ValidatedMarkerRecord,
1601    ) -> Result<MarkerDelivery, StorageRestoreError> {
1602        let restored = self.restore_with_target(
1603            conversation_id,
1604            &record_authority,
1605            MarkerRecordTarget::Bound,
1606            MarkerRecordOccurrence::Undelivered,
1607        );
1608        record_authority.consume();
1609        restored
1610    }
1611
1612    /// Exercises the delivered detached-record gate for adversarial tests.
1613    #[cfg(test)]
1614    pub(super) fn restore_detached_delivered_for_test(
1615        self,
1616        conversation_id: ConversationId,
1617        record_authority: ValidatedMarkerRecord,
1618    ) -> Result<MarkerDelivery, StorageRestoreError> {
1619        let restored = self.restore_with_target(
1620            conversation_id,
1621            &record_authority,
1622            MarkerRecordTarget::Detached,
1623            MarkerRecordOccurrence::Delivered,
1624        );
1625        record_authority.consume();
1626        restored
1627    }
1628
1629    fn restore_detached(
1630        self,
1631        conversation_id: ConversationId,
1632        record_authority: &ValidatedMarkerRecord,
1633    ) -> Result<MarkerDelivery, StorageRestoreError> {
1634        self.restore_with_target(
1635            conversation_id,
1636            record_authority,
1637            MarkerRecordTarget::Detached,
1638            MarkerRecordOccurrence::Undelivered,
1639        )
1640    }
1641
1642    fn restore_with_target(
1643        self,
1644        conversation_id: ConversationId,
1645        record_authority: &ValidatedMarkerRecord,
1646        target: MarkerRecordTarget,
1647        occurrence: MarkerRecordOccurrence,
1648    ) -> Result<MarkerDelivery, StorageRestoreError> {
1649        if record_authority.conversation_id() != conversation_id
1650            || !target.matches(record_authority.target_binding(), self.binding_epoch)
1651            || record_authority.occurrence() != occurrence
1652        {
1653            return Err(StorageRestoreError::StoredEdgeProvenance);
1654        }
1655        let delivery = MarkerDelivery::from_validated_record(record_authority);
1656        if delivery.participant_id() != self.participant_id
1657            || delivery.binding_epoch() != self.binding_epoch
1658            || delivery.marker_delivery_seq() != self.marker_delivery_seq
1659        {
1660            return Err(StorageRestoreError::StoredEdgeProvenance);
1661        }
1662        Ok(delivery)
1663    }
1664}
1665
1666#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1667enum MarkerRecordTarget {
1668    Bound,
1669    Detached,
1670}
1671
1672impl MarkerRecordTarget {
1673    fn matches(self, target: super::FrontierBinding, epoch: BindingEpoch) -> bool {
1674        matches!(
1675            (self, target),
1676            (Self::Bound, super::FrontierBinding::Bound(actual))
1677                | (Self::Detached, super::FrontierBinding::Detached(actual))
1678                if actual == epoch
1679        )
1680    }
1681}
1682
1683#[derive(Debug)]
1684enum MarkerRestoreAuthority<'a> {
1685    Absent,
1686    Record {
1687        conversation_id: ConversationId,
1688        record: &'a ValidatedMarkerRecord,
1689    },
1690}
1691
1692impl MarkerRestoreAuthority<'_> {
1693    const fn require_absent(&self) -> Result<(), StorageRestoreError> {
1694        match self {
1695            Self::Absent => Ok(()),
1696            Self::Record { .. } => Err(StorageRestoreError::StoredEdgeProvenance),
1697        }
1698    }
1699
1700    const fn require_record(
1701        &self,
1702    ) -> Result<(ConversationId, &ValidatedMarkerRecord), StorageRestoreError> {
1703        match self {
1704            Self::Record {
1705                conversation_id,
1706                record,
1707            } if record.conversation_id() == *conversation_id => Ok((*conversation_id, record)),
1708            Self::Absent | Self::Record { .. } => Err(StorageRestoreError::StoredEdgeProvenance),
1709        }
1710    }
1711
1712    fn record_for(
1713        &self,
1714        expected_conversation_id: ConversationId,
1715    ) -> Result<&ValidatedMarkerRecord, StorageRestoreError> {
1716        let (conversation_id, record) = self.require_record()?;
1717        if conversation_id == expected_conversation_id {
1718            Ok(record)
1719        } else {
1720            Err(StorageRestoreError::StoredEdgeProvenance)
1721        }
1722    }
1723}
1724
1725/// Marker-backed cursor provenance retaining its exact delivery predecessor.
1726#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1727pub struct MarkerCursorProgressRestore {
1728    /// Conversation key under which the cursor witness is stored.
1729    pub conversation_id: ConversationId,
1730    /// Cursor witness participant.
1731    pub participant_id: ParticipantId,
1732    /// Cursor witness binding epoch.
1733    pub binding_epoch: BindingEpoch,
1734    /// Required cumulative cursor boundary.
1735    pub through_seq: DeliverySeq,
1736    /// Exact marker accepted by this cursor witness.
1737    pub marker_delivery_seq: DeliverySeq,
1738    /// Exact predecessor delivery proof.
1739    pub delivery: MarkerDeliveryRestore,
1740}
1741
1742impl MarkerCursorProgressRestore {
1743    fn restore_with_debt(
1744        self,
1745        debt: ClosureDebt,
1746        record_authority: &ValidatedMarkerRecord,
1747        target: MarkerRecordTarget,
1748    ) -> Result<ParticipantCursorProgress, StorageRestoreError> {
1749        if self.participant_id != self.delivery.participant_id
1750            || self.binding_epoch != self.delivery.binding_epoch
1751            || self.marker_delivery_seq != self.delivery.marker_delivery_seq
1752            || self.through_seq != self.marker_delivery_seq
1753        {
1754            return Err(StorageRestoreError::StoredEdgeProvenance);
1755        }
1756        let marker = self.delivery.restore_with_target(
1757            self.conversation_id,
1758            record_authority,
1759            target,
1760            MarkerRecordOccurrence::Delivered,
1761        )?;
1762        let state = marker
1763            .delivered(
1764                debt,
1765                Event::marker_delivered(
1766                    self.participant_id,
1767                    self.binding_epoch,
1768                    self.marker_delivery_seq,
1769                ),
1770            )
1771            .map_err(|_| StorageRestoreError::StoredEdgeProvenance)?;
1772        match state {
1773            ClosureState::Owed {
1774                edge: StoredEdge::ParticipantCursorProgress(progress),
1775                ..
1776            } => Ok(progress),
1777            ClosureState::Clear | ClosureState::Owed { .. } => {
1778                Err(StorageRestoreError::StoredEdgeProvenance)
1779            }
1780        }
1781    }
1782}
1783
1784/// Marker-backed detached credential-recovery provenance.
1785#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1786pub struct DetachedCredentialRecoveryRestore {
1787    /// Detached participant.
1788    pub participant_id: ParticipantId,
1789    /// Delivered recovery marker.
1790    pub marker_delivery_seq: DeliverySeq,
1791    /// Dead binding epoch that received the marker.
1792    pub prior_binding_epoch: BindingEpoch,
1793    /// Floor measured by the binding-fate transaction that selected DCR.
1794    pub resulting_floor: DeliverySeq,
1795    /// Exact committed or pending terminal proving the binding fate occurred.
1796    pub terminal: BindingFateTerminalRestore,
1797    /// Exact marker-backed cursor predecessor.
1798    pub progress: MarkerCursorProgressRestore,
1799}
1800
1801impl DetachedCredentialRecoveryRestore {
1802    /// Validates the complete stored recovery audit and rebuilds its copyable
1803    /// description without granting marker-occurrence authority.
1804    ///
1805    /// The resulting value cannot mint a fenced proof by itself. Production
1806    /// must still pair it with the move-only frontier owner whose private
1807    /// `ValidatedMarkerRecord` is consumed by `mint_fenced_attach`.
1808    ///
1809    /// # Errors
1810    ///
1811    /// Returns [`StorageRestoreError`] when terminal, progress, delivery, or
1812    /// identity fields disagree.
1813    pub fn restore_description(self) -> Result<DetachedCredentialRecovery, StorageRestoreError> {
1814        if self.participant_id != self.progress.participant_id
1815            || self.marker_delivery_seq != self.progress.marker_delivery_seq
1816            || self.prior_binding_epoch != self.progress.binding_epoch
1817            || self.progress.through_seq != self.marker_delivery_seq
1818            || self.progress.participant_id != self.progress.delivery.participant_id
1819            || self.progress.binding_epoch != self.progress.delivery.binding_epoch
1820            || self.progress.marker_delivery_seq != self.progress.delivery.marker_delivery_seq
1821        {
1822            return Err(StorageRestoreError::StoredEdgeProvenance);
1823        }
1824        let terminal = self.terminal.restore()?;
1825        if terminal.participant_id() != self.participant_id
1826            || terminal.binding_epoch() != self.prior_binding_epoch
1827            || terminal.conversation_id() != self.progress.conversation_id
1828        {
1829            return Err(StorageRestoreError::StoredEdgeProvenance);
1830        }
1831        Ok(DetachedCredentialRecovery::from_storage_description(
1832            self.progress.conversation_id,
1833            self.participant_id,
1834            self.marker_delivery_seq,
1835            self.prior_binding_epoch,
1836            self.resulting_floor,
1837        ))
1838    }
1839
1840    fn restore_with_debt(
1841        self,
1842        debt: ClosureDebt,
1843        record_authority: &ValidatedMarkerRecord,
1844    ) -> Result<DetachedCredentialRecovery, StorageRestoreError> {
1845        if self.participant_id != self.progress.participant_id
1846            || self.marker_delivery_seq != self.progress.marker_delivery_seq
1847            || self.prior_binding_epoch != self.progress.binding_epoch
1848        {
1849            return Err(StorageRestoreError::StoredEdgeProvenance);
1850        }
1851        let terminal = self.terminal.restore()?;
1852        if terminal.participant_id() != self.participant_id
1853            || terminal.binding_epoch() != self.prior_binding_epoch
1854            || terminal.conversation_id() != self.progress.conversation_id
1855        {
1856            return Err(StorageRestoreError::StoredEdgeProvenance);
1857        }
1858        let progress = self.progress.restore_with_debt(
1859            debt,
1860            record_authority,
1861            MarkerRecordTarget::Detached,
1862        )?;
1863        let successor = progress
1864            .binding_fate(
1865                debt,
1866                terminal,
1867                Event::binding_fate_observed(
1868                    self.participant_id,
1869                    self.prior_binding_epoch,
1870                    self.resulting_floor,
1871                ),
1872            )
1873            .map_err(|_| StorageRestoreError::StoredEdgeProvenance)?;
1874        match successor.into_stored_edge() {
1875            StoredEdge::DetachedCredentialRecovery(edge) => Ok(edge),
1876            _ => Err(StorageRestoreError::StoredEdgeProvenance),
1877        }
1878    }
1879}
1880
1881/// Undelivered-marker release provenance.
1882#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1883pub struct DetachedMarkerReleaseRestore {
1884    /// Conversation key under which the release is stored.
1885    pub conversation_id: ConversationId,
1886    /// Detached participant.
1887    pub participant_id: ParticipantId,
1888    /// Undelivered marker sequence.
1889    pub marker_delivery_seq: DeliverySeq,
1890    /// Dead binding epoch.
1891    pub last_dead_binding_epoch: BindingEpoch,
1892    /// Exact undelivered marker predecessor.
1893    pub delivery: MarkerDeliveryRestore,
1894    /// Which of the two paths formed the release, and what that path left.
1895    pub provenance: DetachedMarkerReleaseProvenanceRestore,
1896}
1897
1898/// What each of the two release paths left behind to be restored from.
1899///
1900/// Both arms exist from this type's birth, so no downstream matcher is broken
1901/// by the distinction being drawn.
1902#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1903pub enum DetachedMarkerReleaseProvenanceRestore {
1904    /// A pre-delivery binding fate, which left the terminal it validated.
1905    ///
1906    /// The floor that fate measured is deliberately absent. The fate reads the
1907    /// participant and epoch out of its event and discards the floor, no
1908    /// restored value depends on it, and the other path never measured one --
1909    /// so a floor here would be a fact nothing validates, nothing reads, and a
1910    /// later reader would believe.
1911    Fate {
1912        /// Exact committed or pending terminal proving the binding fate occurred.
1913        terminal: BindingFateTerminalRestore,
1914    },
1915    /// A frontier marker candidate against an already-dead target epoch, which
1916    /// left nothing: no fate, no terminal, no floor.
1917    Candidate,
1918}
1919
1920impl DetachedMarkerReleaseRestore {
1921    /// Validates the whole stored release audit and rebuilds the live release.
1922    ///
1923    /// The fate path's terminal is checked against this release's own identity
1924    /// exactly as the fate transition checks it. The candidate path has no
1925    /// terminal to check, which is the whole reason the two arms exist.
1926    fn restore_with_marker_record(
1927        self,
1928        record_authority: &ValidatedMarkerRecord,
1929    ) -> Result<DetachedMarkerRelease, StorageRestoreError> {
1930        if self.participant_id != self.delivery.participant_id
1931            || self.marker_delivery_seq != self.delivery.marker_delivery_seq
1932            || self.last_dead_binding_epoch != self.delivery.binding_epoch
1933        {
1934            return Err(StorageRestoreError::StoredEdgeProvenance);
1935        }
1936        let provenance = match self.provenance {
1937            DetachedMarkerReleaseProvenanceRestore::Fate { terminal } => {
1938                let terminal = terminal.restore()?;
1939                if terminal.participant_id() != self.participant_id
1940                    || terminal.binding_epoch() != self.last_dead_binding_epoch
1941                    || terminal.conversation_id() != self.conversation_id
1942                {
1943                    return Err(StorageRestoreError::StoredEdgeProvenance);
1944                }
1945                DetachedMarkerReleaseProvenance::Fate
1946            }
1947            DetachedMarkerReleaseProvenanceRestore::Candidate => {
1948                DetachedMarkerReleaseProvenance::Candidate
1949            }
1950        };
1951        self.delivery
1952            .restore_detached(self.conversation_id, record_authority)?;
1953        Ok(DetachedMarkerRelease::from_storage_description(
1954            self.conversation_id,
1955            self.participant_id,
1956            self.marker_delivery_seq,
1957            self.last_dead_binding_epoch,
1958            provenance,
1959        ))
1960    }
1961}
1962
1963/// Exact ordinary-binding fate provenance for cursor release.
1964///
1965/// Raw fields are deliberately not independently restorable. The participant's
1966/// total snapshot must first prove its unfenced binding origin.
1967///
1968/// ```compile_fail
1969/// use liminal_protocol::lifecycle::OrdinaryBindingFateRestore;
1970///
1971/// fn bypass(raw: OrdinaryBindingFateRestore) {
1972///     let _ = raw.restore();
1973/// }
1974/// ```
1975#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1976pub struct OrdinaryBindingFateRestore {
1977    /// Ordinary-attach authority that owned the no-marker cursor.
1978    pub authority: OrdinaryBindingAuthorityRestore,
1979    /// Exact committed `Died` terminal for that authority.
1980    pub terminal: CommittedBindingTerminalRestore,
1981    /// Floor measured in the binding-fate transaction.
1982    pub resulting_floor: DeliverySeq,
1983}
1984
1985impl OrdinaryBindingFateRestore {
1986    /// Validates the ordinary attach and exact committed-death provenance.
1987    ///
1988    /// # Errors
1989    ///
1990    /// Returns [`StorageRestoreError::StoredEdgeProvenance`] unless the terminal
1991    /// is a `Died` terminal for the exact participant, conversation, and epoch.
1992    fn restore_with_origin(
1993        self,
1994        origin: &BindingOrigin,
1995    ) -> Result<OrdinaryBindingFate, StorageRestoreError> {
1996        let authority = self.authority.restore_with_origin(origin)?;
1997        let terminal = self.terminal.restore()?;
1998        let CommittedBindingTerminal::Died(terminal) = terminal else {
1999            return Err(StorageRestoreError::StoredEdgeProvenance);
2000        };
2001        authority
2002            .binding_fate(terminal, self.resulting_floor)
2003            .map_err(|_| StorageRestoreError::StoredEdgeProvenance)
2004    }
2005}
2006
2007/// Raw durable successor class accepted by detached Leave or fenced attach.
2008#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2009pub enum DebtCompletionRestore {
2010    /// Debt cleared completely.
2011    Clear,
2012    /// Nonzero debt with an independent observer-projection successor.
2013    ObserverProjection {
2014        /// Raw nonzero debt vector.
2015        debt: WideResourceVector,
2016        /// Projection boundary.
2017        through_seq: DeliverySeq,
2018    },
2019    /// Nonzero debt with an independent physical-compaction successor.
2020    PhysicalCompaction {
2021        /// Raw nonzero debt vector.
2022        debt: WideResourceVector,
2023        /// First sequence in the compaction range.
2024        from_floor: DeliverySeq,
2025        /// Inclusive compaction boundary.
2026        through_seq: DeliverySeq,
2027    },
2028}
2029
2030impl DebtCompletionRestore {
2031    /// Validates and rebuilds the restricted clear/OP/PC successor.
2032    ///
2033    /// # Errors
2034    ///
2035    /// Returns [`StorageRestoreError`] for zero debt or an inverted compaction range.
2036    pub fn restore(self) -> Result<DebtCompletion, StorageRestoreError> {
2037        match self {
2038            Self::Clear => Ok(DebtCompletion::clear()),
2039            Self::ObserverProjection { debt, through_seq } => {
2040                let debt = ClosureDebt::new(debt).ok_or(StorageRestoreError::ClosureDebt)?;
2041                Ok(DebtCompletion::observer_projection(
2042                    debt,
2043                    ObserverProjection::new(through_seq),
2044                ))
2045            }
2046            Self::PhysicalCompaction {
2047                debt,
2048                from_floor,
2049                through_seq,
2050            } => {
2051                let debt = ClosureDebt::new(debt).ok_or(StorageRestoreError::ClosureDebt)?;
2052                let edge = PhysicalCompaction::new(from_floor, through_seq)
2053                    .ok_or(StorageRestoreError::StoredEdgeProvenance)?;
2054                Ok(DebtCompletion::physical_compaction(debt, edge))
2055            }
2056        }
2057    }
2058}
2059
2060/// Complete predecessor and event fields for a committed fenced attach.
2061///
2062/// Raw fields remain serializable, but their executable restoration entry point
2063/// is crate-private. Public callers cannot combine them with an occurrence token
2064/// obtained from another transition.
2065///
2066/// ```compile_fail
2067/// use liminal_protocol::lifecycle::FencedAttachCommitRestore;
2068///
2069/// fn bypass() {
2070///     let _ = FencedAttachCommitRestore::restore;
2071/// }
2072/// ```
2073#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2074pub struct FencedAttachCommitRestore {
2075    /// Exact DCR predecessor.
2076    pub predecessor: DetachedCredentialRecoveryRestore,
2077    /// Raw nonzero debt carried by the DCR predecessor.
2078    pub predecessor_debt: WideResourceVector,
2079    /// Event participant; must equal the DCR owner.
2080    pub participant_id: ParticipantId,
2081    /// Event marker; must equal the DCR recovery marker.
2082    pub marker_delivery_seq: DeliverySeq,
2083    /// Event prior epoch; must equal the DCR prior epoch.
2084    pub prior_binding_epoch: BindingEpoch,
2085    /// Newly committed immediately-next binding epoch.
2086    pub new_binding_epoch: BindingEpoch,
2087    /// Floor measured by the fenced-attach transaction.
2088    pub resulting_floor: DeliverySeq,
2089    /// Restricted post-attach closure state.
2090    pub successor: DebtCompletionRestore,
2091}
2092
2093impl FencedAttachCommitRestore {
2094    /// Replays validation from the exact DCR predecessor to the fenced commit.
2095    ///
2096    /// # Errors
2097    ///
2098    /// Returns [`StorageRestoreError`] for any participant, marker, epoch,
2099    /// generation, debt, or successor mismatch.
2100    #[cfg(test)]
2101    pub(super) fn restore(
2102        self,
2103        record_authority: ValidatedMarkerRecord,
2104    ) -> Result<FencedAttachCommit, StorageRestoreError> {
2105        let restored = self.restore_with_record(&record_authority);
2106        record_authority.consume();
2107        restored
2108    }
2109
2110    fn restore_with_record(
2111        self,
2112        record_authority: &ValidatedMarkerRecord,
2113    ) -> Result<FencedAttachCommit, StorageRestoreError> {
2114        let debt =
2115            ClosureDebt::new(self.predecessor_debt).ok_or(StorageRestoreError::ClosureDebt)?;
2116        let predecessor = self.predecessor.restore_with_debt(debt, record_authority)?;
2117        FencedAttachCommit::restore_validated(
2118            predecessor,
2119            record_authority,
2120            debt,
2121            Event::fenced_recovery_committed(
2122                self.participant_id,
2123                self.marker_delivery_seq,
2124                self.prior_binding_epoch,
2125                self.new_binding_epoch,
2126                self.resulting_floor,
2127            ),
2128            self.successor.restore()?,
2129        )
2130        .ok_or(StorageRestoreError::StoredEdgeProvenance)
2131    }
2132}
2133
2134/// Complete fenced-attach predecessor for a recovered binding-fate authority.
2135///
2136/// ```compile_fail
2137/// use liminal_protocol::lifecycle::RecoveredBindingFateRestore;
2138///
2139/// fn bypass() {
2140///     let _ = RecoveredBindingFateRestore::restore;
2141/// }
2142/// ```
2143#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2144pub struct RecoveredBindingFateRestore {
2145    /// Exact fenced-attach commit that installed the recovered epoch.
2146    pub fenced_attach: FencedAttachCommitRestore,
2147    /// Fate event participant.
2148    pub participant_id: ParticipantId,
2149    /// Fate event epoch.
2150    pub binding_epoch: BindingEpoch,
2151    /// Floor measured in the fate transaction.
2152    pub resulting_floor: DeliverySeq,
2153}
2154
2155impl RecoveredBindingFateRestore {
2156    /// Replays the fenced commit and validates exact recovered-epoch fate.
2157    ///
2158    /// # Errors
2159    ///
2160    /// Returns [`StorageRestoreError::StoredEdgeProvenance`] for a wrong
2161    /// participant/epoch or a fenced attach whose successor was already clear.
2162    #[cfg(test)]
2163    pub(super) fn restore(
2164        self,
2165        record_authority: ValidatedMarkerRecord,
2166    ) -> Result<RecoveredBindingFate, StorageRestoreError> {
2167        let restored = self.restore_with_record(&record_authority);
2168        record_authority.consume();
2169        restored
2170    }
2171
2172    fn restore_with_record(
2173        self,
2174        record_authority: &ValidatedMarkerRecord,
2175    ) -> Result<RecoveredBindingFate, StorageRestoreError> {
2176        let commit = self.fenced_attach.restore_with_record(record_authority)?;
2177        commit
2178            .recovered_binding_fate(Event::binding_fate_observed(
2179                self.participant_id,
2180                self.binding_epoch,
2181                self.resulting_floor,
2182            ))
2183            .map_err(|_| StorageRestoreError::StoredEdgeProvenance)
2184    }
2185}
2186
2187/// Durable latent cursor-release suffix while an OP/PC predecessor remains stored.
2188#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2189pub struct PendingRecoveredCursorReleaseRestore {
2190    /// Exact fenced-attach and recovered-fate predecessor.
2191    pub fate: RecoveredBindingFateRestore,
2192    /// Raw nonzero debt remaining after the fate transaction.
2193    pub resulting_debt: WideResourceVector,
2194}
2195
2196impl PendingRecoveredCursorReleaseRestore {
2197    /// Rebuilds the latent suffix only when the exact OP/PC remains current.
2198    ///
2199    /// # Errors
2200    ///
2201    /// Returns [`StorageRestoreError`] if provenance mismatches or the fate
2202    /// covers physical compaction immediately instead of leaving it pending.
2203    #[cfg(test)]
2204    pub(super) fn restore(
2205        self,
2206        record_authority: ValidatedMarkerRecord,
2207    ) -> Result<PendingRecoveredCursorRelease, StorageRestoreError> {
2208        let restored = self.restore_with_record(&record_authority);
2209        record_authority.consume();
2210        restored
2211    }
2212
2213    fn restore_with_record(
2214        self,
2215        record_authority: &ValidatedMarkerRecord,
2216    ) -> Result<PendingRecoveredCursorRelease, StorageRestoreError> {
2217        let authority = self.fate.restore_with_record(record_authority)?;
2218        let predecessor_state = authority.predecessor_state();
2219        let resulting_debt =
2220            ClosureDebt::new(self.resulting_debt).ok_or(StorageRestoreError::ClosureDebt)?;
2221        let transition = match predecessor_state {
2222            ClosureState::Owed {
2223                debt,
2224                edge: StoredEdge::ObserverProjection(edge),
2225            } => edge.apply_recovered_binding_fate(debt, resulting_debt, authority),
2226            ClosureState::Owed {
2227                debt,
2228                edge: StoredEdge::PhysicalCompaction(edge),
2229            } => edge.apply_recovered_binding_fate(debt, resulting_debt, authority),
2230            ClosureState::Clear | ClosureState::Owed { .. } => {
2231                return Err(StorageRestoreError::StoredEdgeProvenance);
2232            }
2233        }
2234        .map_err(|_| StorageRestoreError::StoredEdgeProvenance)?;
2235        match transition {
2236            RecoveredBindingFateTransition::PendingStorage(pending) => Ok(pending),
2237            RecoveredBindingFateTransition::DetachedCursorRelease(_) => {
2238                Err(StorageRestoreError::StoredEdgeProvenance)
2239            }
2240        }
2241    }
2242}
2243
2244/// Exact completion event consuming a latent OP/PC predecessor.
2245#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2246pub enum RecoveredStorageCompletionRestore {
2247    /// Observer projection completed through this boundary.
2248    ObserverProjection {
2249        /// Completion boundary.
2250        through_seq: DeliverySeq,
2251        /// Remaining debt, or `None` when closure clears.
2252        resulting_debt: Option<WideResourceVector>,
2253    },
2254    /// Physical compaction completed this exact range.
2255    PhysicalCompaction {
2256        /// First compacted sequence.
2257        from_floor: DeliverySeq,
2258        /// Inclusive compaction boundary.
2259        through_seq: DeliverySeq,
2260        /// Resulting first-retained floor.
2261        resulting_floor: DeliverySeq,
2262        /// Remaining debt, or `None` when closure clears.
2263        resulting_debt: Option<WideResourceVector>,
2264    },
2265}
2266
2267impl RecoveredStorageCompletionRestore {
2268    fn restore(
2269        self,
2270        pending: PendingRecoveredCursorRelease,
2271    ) -> Result<ClosureState, StorageRestoreError> {
2272        let current = pending.current_state();
2273        match (current, self) {
2274            (
2275                ClosureState::Owed {
2276                    edge: StoredEdge::ObserverProjection(edge),
2277                    ..
2278                },
2279                Self::ObserverProjection {
2280                    through_seq,
2281                    resulting_debt,
2282                },
2283            ) => edge
2284                .complete_after_recovered_binding_fate(
2285                    Event::projection_completed(through_seq),
2286                    optional_debt(resulting_debt)?,
2287                    pending,
2288                )
2289                .map_err(|_| StorageRestoreError::StoredEdgeProvenance),
2290            (
2291                ClosureState::Owed {
2292                    edge: StoredEdge::PhysicalCompaction(edge),
2293                    ..
2294                },
2295                Self::PhysicalCompaction {
2296                    from_floor,
2297                    through_seq,
2298                    resulting_floor,
2299                    resulting_debt,
2300                },
2301            ) => {
2302                let event = Event::compaction_completed(from_floor, through_seq, resulting_floor)
2303                    .ok_or(StorageRestoreError::StoredEdgeProvenance)?;
2304                edge.complete_after_recovered_binding_fate(
2305                    event,
2306                    optional_debt(resulting_debt)?,
2307                    pending,
2308                )
2309                .map_err(|_| StorageRestoreError::StoredEdgeProvenance)
2310            }
2311            _ => Err(StorageRestoreError::StoredEdgeProvenance),
2312        }
2313    }
2314}
2315
2316/// Provenance alternatives capable of constructing `DetachedCursorRelease`.
2317#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2318pub enum DetachedCursorReleaseProvenanceRestore {
2319    /// Direct release from an exact ordinary binding death.
2320    Ordinary(OrdinaryBindingFateRestore),
2321    /// Recovered fate immediately covered physical compaction.
2322    RecoveredDirect {
2323        /// Exact fenced-attach recovered fate.
2324        fate: RecoveredBindingFateRestore,
2325        /// Raw nonzero debt carried by the release.
2326        resulting_debt: WideResourceVector,
2327    },
2328    /// Recovered fate remained latent until exact OP/PC completion.
2329    RecoveredAfterStorage {
2330        /// Exact latent suffix capsule.
2331        pending: PendingRecoveredCursorReleaseRestore,
2332        /// Exact storage completion consuming the predecessor.
2333        completion: RecoveredStorageCompletionRestore,
2334    },
2335}
2336
2337impl DetachedCursorReleaseProvenanceRestore {
2338    const fn ordinary_binding_request(self) -> Option<ActiveBinding> {
2339        match self {
2340            Self::Ordinary(fate) => Some(fate.authority.binding),
2341            Self::RecoveredDirect { .. } | Self::RecoveredAfterStorage { .. } => None,
2342        }
2343    }
2344
2345    const fn marker_record_request(self) -> Option<MarkerRecordRequest> {
2346        match self {
2347            Self::Ordinary(_) => None,
2348            Self::RecoveredDirect { fate, .. } => Some(MarkerRecordRequest::recovered(
2349                fate.participant_id,
2350                fate.fenced_attach.marker_delivery_seq,
2351                fate.fenced_attach.prior_binding_epoch,
2352                fate.fenced_attach.new_binding_epoch,
2353            )),
2354            Self::RecoveredAfterStorage { pending, .. } => Some(MarkerRecordRequest::recovered(
2355                pending.fate.participant_id,
2356                pending.fate.fenced_attach.marker_delivery_seq,
2357                pending.fate.fenced_attach.prior_binding_epoch,
2358                pending.fate.fenced_attach.new_binding_epoch,
2359            )),
2360        }
2361    }
2362
2363    fn restore_state(
2364        self,
2365        debt: ClosureDebt,
2366        marker_authority: &MarkerRestoreAuthority<'_>,
2367        ordinary_origin: Option<&BindingOrigin>,
2368    ) -> Result<ClosureState, StorageRestoreError> {
2369        match self {
2370            Self::Ordinary(provenance) => {
2371                marker_authority.require_absent()?;
2372                let origin = ordinary_origin.ok_or(StorageRestoreError::StoredEdgeProvenance)?;
2373                Ok(provenance
2374                    .restore_with_origin(origin)?
2375                    .into_direct_state(debt))
2376            }
2377            Self::RecoveredDirect {
2378                fate,
2379                resulting_debt,
2380            } => {
2381                let (_, record_authority) = marker_authority.require_record()?;
2382                let authority = fate.restore_with_record(record_authority)?;
2383                let predecessor = authority.predecessor_state();
2384                let resulting_debt =
2385                    ClosureDebt::new(resulting_debt).ok_or(StorageRestoreError::ClosureDebt)?;
2386                let transition = match predecessor {
2387                    ClosureState::Owed {
2388                        debt: predecessor_debt,
2389                        edge: StoredEdge::PhysicalCompaction(edge),
2390                    } => edge.apply_recovered_binding_fate(
2391                        predecessor_debt,
2392                        resulting_debt,
2393                        authority,
2394                    ),
2395                    ClosureState::Clear | ClosureState::Owed { .. } => {
2396                        return Err(StorageRestoreError::StoredEdgeProvenance);
2397                    }
2398                }
2399                .map_err(|_| StorageRestoreError::StoredEdgeProvenance)?;
2400                match transition {
2401                    RecoveredBindingFateTransition::DetachedCursorRelease(release)
2402                        if release.debt() == debt =>
2403                    {
2404                        Ok(release.into_state())
2405                    }
2406                    RecoveredBindingFateTransition::PendingStorage(_)
2407                    | RecoveredBindingFateTransition::DetachedCursorRelease(_) => {
2408                        Err(StorageRestoreError::StoredEdgeProvenance)
2409                    }
2410                }
2411            }
2412            Self::RecoveredAfterStorage {
2413                pending,
2414                completion,
2415            } => {
2416                let (_, record_authority) = marker_authority.require_record()?;
2417                let state = completion.restore(pending.restore_with_record(record_authority)?)?;
2418                match state {
2419                    ClosureState::Owed {
2420                        debt: restored_debt,
2421                        edge: StoredEdge::DetachedCursorRelease(_),
2422                    } if restored_debt == debt => Ok(state),
2423                    ClosureState::Clear | ClosureState::Owed { .. } => {
2424                        Err(StorageRestoreError::StoredEdgeProvenance)
2425                    }
2426                }
2427            }
2428        }
2429    }
2430}
2431
2432/// Exact seven-kind stored-edge representation with provenance where required.
2433#[allow(clippy::large_enum_variant)]
2434#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2435pub enum StoredEdgeRestore {
2436    /// Observer projection.
2437    ObserverProjection {
2438        /// Projection boundary.
2439        through_seq: DeliverySeq,
2440    },
2441    /// Physical compaction.
2442    PhysicalCompaction {
2443        /// First compacted sequence.
2444        from_floor: DeliverySeq,
2445        /// Inclusive compaction boundary.
2446        through_seq: DeliverySeq,
2447    },
2448    /// Planned exact marker delivery.
2449    MarkerDelivery(MarkerDeliveryRestore),
2450    /// Continuous cursor witness carrying ordinary-attach provenance.
2451    ParticipantCursorProgressContinuous {
2452        /// Stored edge participant.
2453        participant_id: ParticipantId,
2454        /// Stored edge binding epoch.
2455        binding_epoch: BindingEpoch,
2456        /// Stored required cursor boundary.
2457        through_seq: DeliverySeq,
2458        /// Exact ordinary-attach predecessor.
2459        authority: OrdinaryBindingAuthorityRestore,
2460    },
2461    /// Marker-backed cursor witness carrying delivery provenance.
2462    ParticipantCursorProgressMarker(MarkerCursorProgressRestore),
2463    /// Detached credential recovery carrying marker-ack provenance.
2464    DetachedCredentialRecovery(DetachedCredentialRecoveryRestore),
2465    /// Detached undelivered-marker release carrying delivery provenance.
2466    DetachedMarkerRelease(DetachedMarkerReleaseRestore),
2467    /// Detached cursor release carrying ordinary or fenced provenance.
2468    DetachedCursorRelease {
2469        /// Stored edge participant.
2470        participant_id: ParticipantId,
2471        /// Stored edge dead binding epoch.
2472        last_dead_binding_epoch: BindingEpoch,
2473        /// Provenance path that alone can construct the release.
2474        provenance: DetachedCursorReleaseProvenanceRestore,
2475    },
2476}
2477
2478impl StoredEdgeRestore {
2479    const fn ordinary_binding_request(self) -> Option<ActiveBinding> {
2480        match self {
2481            Self::ParticipantCursorProgressContinuous { authority, .. } => Some(authority.binding),
2482            Self::DetachedCursorRelease { provenance, .. } => provenance.ordinary_binding_request(),
2483            Self::ObserverProjection { .. }
2484            | Self::PhysicalCompaction { .. }
2485            | Self::MarkerDelivery(_)
2486            | Self::ParticipantCursorProgressMarker(_)
2487            | Self::DetachedCredentialRecovery(_)
2488            | Self::DetachedMarkerRelease(_) => None,
2489        }
2490    }
2491
2492    const fn marker_record_request(self) -> Option<MarkerRecordRequest> {
2493        match self {
2494            Self::MarkerDelivery(value) => Some(MarkerRecordRequest::planned(
2495                value.participant_id,
2496                value.marker_delivery_seq,
2497                super::FrontierBinding::Bound(value.binding_epoch),
2498            )),
2499            Self::ParticipantCursorProgressMarker(value) => Some(MarkerRecordRequest::delivered(
2500                value.participant_id,
2501                value.marker_delivery_seq,
2502                super::FrontierBinding::Bound(value.binding_epoch),
2503            )),
2504            Self::DetachedCredentialRecovery(value) => Some(MarkerRecordRequest::delivered(
2505                value.participant_id,
2506                value.marker_delivery_seq,
2507                super::FrontierBinding::Detached(value.prior_binding_epoch),
2508            )),
2509            Self::DetachedMarkerRelease(value) => Some(MarkerRecordRequest::planned(
2510                value.participant_id,
2511                value.marker_delivery_seq,
2512                super::FrontierBinding::Detached(value.last_dead_binding_epoch),
2513            )),
2514            Self::DetachedCursorRelease { provenance, .. } => provenance.marker_record_request(),
2515            Self::ObserverProjection { .. }
2516            | Self::PhysicalCompaction { .. }
2517            | Self::ParticipantCursorProgressContinuous { .. } => None,
2518        }
2519    }
2520
2521    fn restore_with_debt(
2522        self,
2523        debt: ClosureDebt,
2524        marker_authority: &MarkerRestoreAuthority<'_>,
2525        ordinary_origin: Option<&BindingOrigin>,
2526    ) -> Result<StoredEdge, StorageRestoreError> {
2527        match self {
2528            Self::ObserverProjection { through_seq } => {
2529                marker_authority.require_absent()?;
2530                Ok(StoredEdge::ObserverProjection(ObserverProjection::new(
2531                    through_seq,
2532                )))
2533            }
2534            Self::PhysicalCompaction {
2535                from_floor,
2536                through_seq,
2537            } => {
2538                marker_authority.require_absent()?;
2539                PhysicalCompaction::new(from_floor, through_seq)
2540                    .map(StoredEdge::PhysicalCompaction)
2541                    .ok_or(StorageRestoreError::StoredEdgeProvenance)
2542            }
2543            Self::MarkerDelivery(value) => {
2544                let (conversation_id, record_authority) = marker_authority.require_record()?;
2545                value
2546                    .restore_with_target(
2547                        conversation_id,
2548                        record_authority,
2549                        MarkerRecordTarget::Bound,
2550                        MarkerRecordOccurrence::Undelivered,
2551                    )
2552                    .map(StoredEdge::MarkerDelivery)
2553            }
2554            Self::ParticipantCursorProgressContinuous {
2555                participant_id,
2556                binding_epoch,
2557                through_seq,
2558                authority,
2559            } => {
2560                marker_authority.require_absent()?;
2561                let origin = ordinary_origin.ok_or(StorageRestoreError::StoredEdgeProvenance)?;
2562                ParticipantCursorProgress::restore_continuous(
2563                    authority.restore_with_origin(origin)?,
2564                    participant_id,
2565                    binding_epoch,
2566                    through_seq,
2567                )
2568                .map(StoredEdge::ParticipantCursorProgress)
2569                .ok_or(StorageRestoreError::StoredEdgeProvenance)
2570            }
2571            Self::ParticipantCursorProgressMarker(value) => {
2572                let record_authority = marker_authority.record_for(value.conversation_id)?;
2573                value
2574                    .restore_with_debt(debt, record_authority, MarkerRecordTarget::Bound)
2575                    .map(StoredEdge::ParticipantCursorProgress)
2576            }
2577            Self::DetachedCredentialRecovery(value) => {
2578                let record_authority =
2579                    marker_authority.record_for(value.progress.conversation_id)?;
2580                value
2581                    .restore_with_debt(debt, record_authority)
2582                    .map(StoredEdge::DetachedCredentialRecovery)
2583            }
2584            Self::DetachedMarkerRelease(value) => {
2585                let record_authority = marker_authority.record_for(value.conversation_id)?;
2586                value
2587                    .restore_with_marker_record(record_authority)
2588                    .map(StoredEdge::DetachedMarkerRelease)
2589            }
2590            Self::DetachedCursorRelease {
2591                participant_id,
2592                last_dead_binding_epoch,
2593                provenance,
2594            } => {
2595                let state = provenance.restore_state(debt, marker_authority, ordinary_origin)?;
2596                match state {
2597                    ClosureState::Owed {
2598                        edge: StoredEdge::DetachedCursorRelease(edge),
2599                        ..
2600                    } if edge.participant_id() == participant_id
2601                        && edge.last_dead_binding_epoch() == last_dead_binding_epoch =>
2602                    {
2603                        Ok(StoredEdge::DetachedCursorRelease(edge))
2604                    }
2605                    ClosureState::Clear | ClosureState::Owed { .. } => {
2606                        Err(StorageRestoreError::StoredEdgeProvenance)
2607                    }
2608                }
2609            }
2610        }
2611    }
2612}
2613
2614/// Raw closure state; a stored edge always carries a raw nonzero debt vector.
2615#[allow(clippy::large_enum_variant)]
2616#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2617pub enum ClosureStateRestore {
2618    /// No edge and zero debt.
2619    Clear,
2620    /// One exact stored edge and raw componentwise debt.
2621    Owed {
2622        /// Raw debt, validated nonzero during restoration.
2623        debt: WideResourceVector,
2624        /// Exact edge and required predecessor provenance.
2625        edge: StoredEdgeRestore,
2626    },
2627}
2628
2629impl ClosureStateRestore {
2630    const fn ordinary_binding_request(self) -> Option<ActiveBinding> {
2631        match self {
2632            Self::Clear => None,
2633            Self::Owed { edge, .. } => edge.ordinary_binding_request(),
2634        }
2635    }
2636
2637    const fn marker_record_request(self) -> Option<MarkerRecordRequest> {
2638        match self {
2639            Self::Clear => None,
2640            Self::Owed { edge, .. } => edge.marker_record_request(),
2641        }
2642    }
2643
2644    /// Validates and rebuilds one closure state.
2645    ///
2646    /// # Errors
2647    ///
2648    /// Returns [`StorageRestoreError`] for zero owed debt, an invalid range, or
2649    /// any opaque edge whose supplied predecessor provenance does not match.
2650    pub fn restore(self) -> Result<ClosureState, StorageRestoreError> {
2651        self.restore_with_authority(&MarkerRestoreAuthority::Absent, None)
2652    }
2653
2654    /// Restores exactly one marker-derived edge after claim/log prevalidation.
2655    ///
2656    /// The marker token is conversation-bound and fixes whether its current
2657    /// target is live (`MarkerDelivery`/marker PCP) or detached (DMR/DCR and
2658    /// recovered cursor-release history).
2659    ///
2660    /// # Errors
2661    ///
2662    /// Returns [`StorageRestoreError::StoredEdgeProvenance`] when the raw edge
2663    /// is not marker-derived, the token belongs to another conversation, or
2664    /// the retained record has the wrong target state, epoch, participant, or
2665    /// delivery sequence.
2666    pub(super) fn restore_with_marker_record(
2667        self,
2668        conversation_id: ConversationId,
2669        record: ValidatedMarkerRecord,
2670    ) -> Result<ClosureState, StorageRestoreError> {
2671        let restored = self.restore_with_authority(
2672            &MarkerRestoreAuthority::Record {
2673                conversation_id,
2674                record: &record,
2675            },
2676            None,
2677        );
2678        record.consume();
2679        restored
2680    }
2681
2682    fn restore_with_binding_origin(
2683        self,
2684        origin: &BindingOrigin,
2685    ) -> Result<ClosureState, StorageRestoreError> {
2686        self.restore_with_authority(&MarkerRestoreAuthority::Absent, Some(origin))
2687    }
2688
2689    fn restore_with_authority(
2690        self,
2691        marker_authority: &MarkerRestoreAuthority<'_>,
2692        ordinary_origin: Option<&BindingOrigin>,
2693    ) -> Result<ClosureState, StorageRestoreError> {
2694        match self {
2695            Self::Clear => {
2696                marker_authority.require_absent()?;
2697                Ok(ClosureState::Clear)
2698            }
2699            Self::Owed { debt, edge } => {
2700                let debt = ClosureDebt::new(debt).ok_or(StorageRestoreError::ClosureDebt)?;
2701                Ok(ClosureState::Owed {
2702                    debt,
2703                    edge: edge.restore_with_debt(debt, marker_authority, ordinary_origin)?,
2704                })
2705            }
2706        }
2707    }
2708}
2709
2710fn optional_debt(
2711    raw: Option<WideResourceVector>,
2712) -> Result<Option<ClosureDebt>, StorageRestoreError> {
2713    raw.map(|value| ClosureDebt::new(value).ok_or(StorageRestoreError::ClosureDebt))
2714        .transpose()
2715}