Skip to main content

lodestone_types/
mutation_lifecycle.rs

1//! Canonical interpretation of one exact mutation across authority layers.
2//!
3//! This is a transport-neutral derived model, not a serialized receipt or a
4//! second completion authority. Each durable owner retains its existing
5//! receipt/state and maps an exact read-back observation into this vocabulary.
6
7use std::error::Error;
8use std::fmt::{self, Display, Formatter};
9
10/// Closed owner set for externally recoverable mutation lifecycles.
11///
12/// This is an unencoded compile-time contract used to bind an indeterminate
13/// emitter to the authority-specific resolver that owns its outcome.
14#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
15pub enum MutationLifecycleAuthority {
16    Catalog,
17    Security,
18    Audit,
19    Wal,
20    Manifest,
21    Task,
22    Portability,
23    ClusterControl,
24    State,
25}
26
27pub const MUTATION_LIFECYCLE_AUTHORITIES: [MutationLifecycleAuthority; 9] = [
28    MutationLifecycleAuthority::Catalog,
29    MutationLifecycleAuthority::Security,
30    MutationLifecycleAuthority::Audit,
31    MutationLifecycleAuthority::Wal,
32    MutationLifecycleAuthority::Manifest,
33    MutationLifecycleAuthority::Task,
34    MutationLifecycleAuthority::Portability,
35    MutationLifecycleAuthority::ClusterControl,
36    MutationLifecycleAuthority::State,
37];
38
39/// Authority-owned projection from one durable resolution into the shared
40/// mutation lifecycle. Implementations may only interpret their existing
41/// receipt; they do not store a second completion fact.
42pub trait MutationLifecycleResolver<Locator> {
43    fn mutation_lifecycle_authority(&self, locator: &Locator) -> MutationLifecycleAuthority;
44
45    fn resolve_mutation_lifecycle(
46        &self,
47        locator: &Locator,
48        visible: bool,
49    ) -> Result<MutationLifecycleObservation, MutationLifecycleError>;
50}
51
52/// Identity that must remain byte-exact from prepare through visibility.
53#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
54pub struct MutationReplayIdentity {
55    authority_binding: [u8; 32],
56    operation_identity: [u8; 32],
57    semantic_digest: [u8; 32],
58}
59
60impl MutationReplayIdentity {
61    pub fn new(
62        authority_binding: [u8; 32],
63        operation_identity: [u8; 32],
64        semantic_digest: [u8; 32],
65    ) -> Result<Self, MutationLifecycleError> {
66        if authority_binding == [0; 32] {
67            return Err(MutationLifecycleError::ZeroAuthorityBinding);
68        }
69        if operation_identity == [0; 32] {
70            return Err(MutationLifecycleError::ZeroOperationIdentity);
71        }
72        if semantic_digest == [0; 32] {
73            return Err(MutationLifecycleError::ZeroSemanticDigest);
74        }
75        Ok(Self {
76            authority_binding,
77            operation_identity,
78            semantic_digest,
79        })
80    }
81
82    #[must_use]
83    pub const fn authority_binding(self) -> [u8; 32] {
84        self.authority_binding
85    }
86
87    #[must_use]
88    pub const fn operation_identity(self) -> [u8; 32] {
89        self.operation_identity
90    }
91
92    #[must_use]
93    pub const fn semantic_digest(self) -> [u8; 32] {
94        self.semantic_digest
95    }
96}
97
98/// The four monotonic lifecycle stages shared by mutation authorities.
99#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
100pub enum MutationLifecycleStage {
101    NotSubmitted,
102    Prepared,
103    Committed,
104    Visible,
105}
106
107/// Exact meaning of a prepared observation.
108#[derive(Clone, Copy, Debug, Eq, PartialEq)]
109pub enum PreparedMutationDisposition {
110    /// The durable prepare proves that replaying the same identity is safe.
111    SafelyReplayable,
112    /// Submission is in progress; callers may observe but must not resubmit.
113    StillPending,
114}
115
116/// Closed result of resolving a possibly submitted mutation.
117#[derive(Clone, Copy, Debug, Eq, PartialEq)]
118pub enum SubmittedIndeterminateResolution {
119    Committed { visible: bool },
120    SafelyReplayable,
121    StillPending,
122    ProvenNotSubmitted,
123}
124
125#[derive(Clone, Copy, Debug, Eq, PartialEq)]
126enum MutationLifecycleState {
127    NotSubmitted,
128    Prepared(PreparedMutationDisposition),
129    Committed,
130    Visible,
131}
132
133/// One exact authority observation in the canonical lifecycle.
134#[derive(Clone, Copy, Debug, Eq, PartialEq)]
135pub struct MutationLifecycleObservation {
136    replay_identity: MutationReplayIdentity,
137    state: MutationLifecycleState,
138}
139
140impl MutationLifecycleObservation {
141    #[must_use]
142    pub const fn proven_not_submitted(replay_identity: MutationReplayIdentity) -> Self {
143        Self {
144            replay_identity,
145            state: MutationLifecycleState::NotSubmitted,
146        }
147    }
148
149    #[must_use]
150    pub const fn prepared(replay_identity: MutationReplayIdentity, disposition: PreparedMutationDisposition) -> Self {
151        Self {
152            replay_identity,
153            state: MutationLifecycleState::Prepared(disposition),
154        }
155    }
156
157    #[must_use]
158    pub const fn committed(replay_identity: MutationReplayIdentity) -> Self {
159        Self {
160            replay_identity,
161            state: MutationLifecycleState::Committed,
162        }
163    }
164
165    #[must_use]
166    pub const fn visible(replay_identity: MutationReplayIdentity) -> Self {
167        Self {
168            replay_identity,
169            state: MutationLifecycleState::Visible,
170        }
171    }
172
173    #[must_use]
174    pub const fn replay_identity(self) -> MutationReplayIdentity {
175        self.replay_identity
176    }
177
178    #[must_use]
179    pub const fn stage(self) -> MutationLifecycleStage {
180        match self.state {
181            MutationLifecycleState::NotSubmitted => MutationLifecycleStage::NotSubmitted,
182            MutationLifecycleState::Prepared(_) => MutationLifecycleStage::Prepared,
183            MutationLifecycleState::Committed => MutationLifecycleStage::Committed,
184            MutationLifecycleState::Visible => MutationLifecycleStage::Visible,
185        }
186    }
187
188    #[must_use]
189    pub const fn prepared_disposition(self) -> Option<PreparedMutationDisposition> {
190        match self.state {
191            MutationLifecycleState::Prepared(disposition) => Some(disposition),
192            MutationLifecycleState::NotSubmitted
193            | MutationLifecycleState::Committed
194            | MutationLifecycleState::Visible => None,
195        }
196    }
197
198    #[must_use]
199    pub const fn resolve_submitted_indeterminate(self) -> SubmittedIndeterminateResolution {
200        match self.state {
201            MutationLifecycleState::NotSubmitted => SubmittedIndeterminateResolution::ProvenNotSubmitted,
202            MutationLifecycleState::Prepared(PreparedMutationDisposition::SafelyReplayable) => {
203                SubmittedIndeterminateResolution::SafelyReplayable
204            }
205            MutationLifecycleState::Prepared(PreparedMutationDisposition::StillPending) => {
206                SubmittedIndeterminateResolution::StillPending
207            }
208            MutationLifecycleState::Committed => SubmittedIndeterminateResolution::Committed { visible: false },
209            MutationLifecycleState::Visible => SubmittedIndeterminateResolution::Committed { visible: true },
210        }
211    }
212
213    pub fn require_monotonic_successor(self, successor: Self) -> Result<(), MutationLifecycleError> {
214        if self.replay_identity != successor.replay_identity {
215            return Err(MutationLifecycleError::ReplayIdentityChanged);
216        }
217        let stage = self.stage();
218        let successor_stage = successor.stage();
219        if successor_stage < stage {
220            return Err(MutationLifecycleError::StageRegressed {
221                from: stage,
222                to: successor_stage,
223            });
224        }
225        Ok(())
226    }
227}
228
229#[derive(Clone, Copy, Debug, Eq, PartialEq)]
230pub enum MutationLifecycleError {
231    ZeroAuthorityBinding,
232    ZeroOperationIdentity,
233    ZeroSemanticDigest,
234    ReplayIdentityChanged,
235    StageRegressed {
236        from: MutationLifecycleStage,
237        to: MutationLifecycleStage,
238    },
239    OutcomeEvidenceRetired,
240}
241
242impl Display for MutationLifecycleError {
243    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
244        match self {
245            Self::ZeroAuthorityBinding => formatter.write_str("mutation authority binding cannot be zero"),
246            Self::ZeroOperationIdentity => formatter.write_str("mutation operation identity cannot be zero"),
247            Self::ZeroSemanticDigest => formatter.write_str("mutation semantic digest cannot be zero"),
248            Self::ReplayIdentityChanged => {
249                formatter.write_str("mutation replay identity changed across lifecycle observations")
250            }
251            Self::StageRegressed { from, to } => {
252                write!(formatter, "mutation lifecycle regressed from {from:?} to {to:?}")
253            }
254            Self::OutcomeEvidenceRetired => {
255                formatter.write_str("mutation outcome evidence retired before deterministic resolution")
256            }
257        }
258    }
259}
260
261impl Error for MutationLifecycleError {}
262
263#[cfg(test)]
264mod tests {
265    use std::collections::BTreeSet;
266
267    use super::*;
268
269    fn replay_identity(value: u8) -> Result<MutationReplayIdentity, MutationLifecycleError> {
270        MutationReplayIdentity::new(
271            [value; 32],
272            [value.saturating_add(1); 32],
273            [value.saturating_add(2); 32],
274        )
275    }
276
277    #[test]
278    fn submitted_indeterminate_has_exactly_four_resolution_classes() -> Result<(), Box<dyn Error>> {
279        let identity = replay_identity(1)?;
280        assert_eq!(
281            MutationLifecycleObservation::proven_not_submitted(identity).resolve_submitted_indeterminate(),
282            SubmittedIndeterminateResolution::ProvenNotSubmitted
283        );
284        assert_eq!(
285            MutationLifecycleObservation::prepared(identity, PreparedMutationDisposition::SafelyReplayable)
286                .resolve_submitted_indeterminate(),
287            SubmittedIndeterminateResolution::SafelyReplayable
288        );
289        assert_eq!(
290            MutationLifecycleObservation::prepared(identity, PreparedMutationDisposition::StillPending)
291                .resolve_submitted_indeterminate(),
292            SubmittedIndeterminateResolution::StillPending
293        );
294        assert_eq!(
295            MutationLifecycleObservation::committed(identity).resolve_submitted_indeterminate(),
296            SubmittedIndeterminateResolution::Committed { visible: false }
297        );
298        assert_eq!(
299            MutationLifecycleObservation::visible(identity).resolve_submitted_indeterminate(),
300            SubmittedIndeterminateResolution::Committed { visible: true }
301        );
302        Ok(())
303    }
304
305    #[test]
306    fn lifecycle_authority_registry_is_closed_and_duplicate_free() {
307        assert_eq!(MUTATION_LIFECYCLE_AUTHORITIES.len(), 9);
308        assert_eq!(
309            MUTATION_LIFECYCLE_AUTHORITIES
310                .into_iter()
311                .collect::<BTreeSet<_>>()
312                .len(),
313            MUTATION_LIFECYCLE_AUTHORITIES.len()
314        );
315    }
316
317    #[test]
318    fn lifecycle_is_monotonic_and_identity_exact() -> Result<(), Box<dyn Error>> {
319        let identity = replay_identity(7)?;
320        let not_submitted = MutationLifecycleObservation::proven_not_submitted(identity);
321        let replayable =
322            MutationLifecycleObservation::prepared(identity, PreparedMutationDisposition::SafelyReplayable);
323        let pending = MutationLifecycleObservation::prepared(identity, PreparedMutationDisposition::StillPending);
324        let committed = MutationLifecycleObservation::committed(identity);
325        let visible = MutationLifecycleObservation::visible(identity);
326        not_submitted.require_monotonic_successor(replayable)?;
327        replayable.require_monotonic_successor(pending)?;
328        pending.require_monotonic_successor(replayable)?;
329        pending.require_monotonic_successor(committed)?;
330        committed.require_monotonic_successor(visible)?;
331        assert!(matches!(
332            visible.require_monotonic_successor(committed),
333            Err(MutationLifecycleError::StageRegressed { .. })
334        ));
335        assert_eq!(
336            committed.require_monotonic_successor(MutationLifecycleObservation::visible(replay_identity(8)?,)),
337            Err(MutationLifecycleError::ReplayIdentityChanged)
338        );
339        Ok(())
340    }
341
342    #[test]
343    fn replay_identity_refuses_ambiguous_components() {
344        assert_eq!(
345            MutationReplayIdentity::new([0; 32], [1; 32], [2; 32]),
346            Err(MutationLifecycleError::ZeroAuthorityBinding)
347        );
348        assert_eq!(
349            MutationReplayIdentity::new([1; 32], [0; 32], [2; 32]),
350            Err(MutationLifecycleError::ZeroOperationIdentity)
351        );
352        assert_eq!(
353            MutationReplayIdentity::new([1; 32], [2; 32], [0; 32]),
354            Err(MutationLifecycleError::ZeroSemanticDigest)
355        );
356    }
357}