Skip to main content

lodestone_client/
exact_workflows.rs

1//! Recovery-correct native mutation workflows.
2//!
3//! The server remains the effect authority. This module owns only the
4//! caller-side identity, deadline, and resolver state needed to avoid minting
5//! a replacement effect after response loss.
6
7use std::error::Error;
8use std::fmt::{self, Display, Formatter};
9use std::future::Future;
10use std::path::{Path, PathBuf};
11use std::pin::Pin;
12use std::sync::Arc;
13use std::time::{SystemTime, UNIX_EPOCH};
14
15use aws_lc_rs::digest;
16use lodestone_wire::{
17    Digest, ErrorCode, GetIndexRequest, Identity, IndexResourceSpec, Locator, MutationOutcome,
18    ObserveAppendCommittedRequest, PrepareAppendBulkRequest, PrepareCreateIndexRequest, PrepareDocumentMutationRequest,
19    ResolveAppendBulkRequest, ResolveCatalogActionRequest, ResolveDocumentMutationRequest, Resource,
20    RetainedAppendBulkOutcome, RetainedAppendItem, RetainedAppendItemOutcome, RetainedAppendObservationOutcome,
21    RetainedAppendObservationState, RetainedAppendRetrySafety, RetainedAppendState, SubmitAppendBulkRequest,
22    SubmitCatalogActionRequest, SubmitDocumentMutationRequest, U64,
23};
24use serde::{Deserialize, Serialize};
25
26use crate::workflow_journal::sealed;
27use crate::{
28    CallCancellation, CallOptions, Client, ClientError, LocalOperatorWorkflowKind, SubmissionDisposition,
29    WorkflowJournal, WorkflowJournalError, WorkflowJournalIntent, WorkflowJournalTransition,
30};
31
32pub const EXACT_MUTATION_FORMAT_VERSION: u16 = 2;
33pub const DEFAULT_EXACT_WORKFLOW_STORE_CONFLICT_LIMIT: u32 = 8;
34const MAX_EFFECT_CALL_MILLIS: u64 = 599_000;
35const MAX_SAFE_REVISION: u64 = 9_007_199_254_740_991;
36
37/// Returns the catalog submit discriminator for a prepared index creation.
38///
39/// The preparation authority selects retained-append creation whenever the
40/// caller explicitly requests retained event identity, so recovery must
41/// preserve that same discriminator at submit time.
42#[must_use]
43pub const fn create_index_action_kind(retained_event_identity: Option<bool>) -> &'static str {
44    if matches!(retained_event_identity, Some(true)) {
45        "create_retained_append_log_index"
46    } else {
47        "create_index"
48    }
49}
50
51/// Semantic owner of one recovery record.
52#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
53#[serde(rename_all = "snake_case")]
54pub enum ExactMutationKindV1 {
55    Append,
56    Document,
57    Index,
58}
59
60/// Closed result vocabulary shared by all exact mutation helpers.
61#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
62#[serde(rename_all = "snake_case")]
63pub enum ExactMutationOutcomeV1 {
64    AlreadySatisfied,
65    Applied,
66    Resolved,
67    Conflict,
68    NotSubmitted,
69    Indeterminate,
70    KnownRetired,
71    EffectDeadlineExpired,
72    ClockRegression,
73}
74
75#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
76#[serde(rename_all = "snake_case")]
77enum ExactSubmissionDispositionV1 {
78    NotSubmitted,
79    Submitted,
80    Resolved,
81    KnownRetired,
82    Indeterminate,
83}
84
85/// Durable progress of one exact caller-side mutation workflow.
86#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
87#[serde(rename_all = "snake_case")]
88pub enum ExactWorkflowPhaseV2 {
89    Initialized,
90    Prepared,
91    SubmissionStarted,
92    SubmissionObserved,
93    Terminal,
94}
95
96impl From<SubmissionDisposition> for ExactSubmissionDispositionV1 {
97    fn from(value: SubmissionDisposition) -> Self {
98        match value {
99            SubmissionDisposition::NotSubmitted => Self::NotSubmitted,
100            SubmissionDisposition::Submitted => Self::Submitted,
101            SubmissionDisposition::Resolved => Self::Resolved,
102            SubmissionDisposition::KnownRetired => Self::KnownRetired,
103            SubmissionDisposition::Indeterminate => Self::Indeterminate,
104        }
105    }
106}
107
108fn exact_submission_disposition(outcome: &MutationOutcome) -> ExactSubmissionDispositionV1 {
109    match outcome {
110        MutationOutcome::Terminal => ExactSubmissionDispositionV1::Resolved,
111        MutationOutcome::Pending { .. } => ExactSubmissionDispositionV1::Submitted,
112        MutationOutcome::Indeterminate { .. } | MutationOutcome::ResolutionRequired { .. } => {
113            ExactSubmissionDispositionV1::Indeterminate
114        }
115    }
116}
117
118fn resolution_disposition(outcome: ExactMutationOutcomeV1) -> ExactSubmissionDispositionV1 {
119    match outcome {
120        ExactMutationOutcomeV1::NotSubmitted | ExactMutationOutcomeV1::EffectDeadlineExpired => {
121            ExactSubmissionDispositionV1::NotSubmitted
122        }
123        ExactMutationOutcomeV1::KnownRetired => ExactSubmissionDispositionV1::KnownRetired,
124        ExactMutationOutcomeV1::Indeterminate | ExactMutationOutcomeV1::ClockRegression => {
125            ExactSubmissionDispositionV1::Indeterminate
126        }
127        ExactMutationOutcomeV1::AlreadySatisfied
128        | ExactMutationOutcomeV1::Applied
129        | ExactMutationOutcomeV1::Resolved
130        | ExactMutationOutcomeV1::Conflict => ExactSubmissionDispositionV1::Resolved,
131    }
132}
133
134fn append_resolution_disposition(outcome: &RetainedAppendBulkOutcome) -> ExactSubmissionDispositionV1 {
135    if outcome
136        .items
137        .iter()
138        .any(|item| item.state == RetainedAppendState::SubmittedIndeterminate)
139    {
140        ExactSubmissionDispositionV1::Indeterminate
141    } else if outcome
142        .items
143        .iter()
144        .any(|item| item.state == RetainedAppendState::Retired)
145    {
146        ExactSubmissionDispositionV1::KnownRetired
147    } else if outcome.items.iter().any(|item| {
148        matches!(
149            item.state,
150            RetainedAppendState::Prepared
151                | RetainedAppendState::NotSubmittedRetryable
152                | RetainedAppendState::NotSubmittedTerminal
153        )
154    }) {
155        ExactSubmissionDispositionV1::NotSubmitted
156    } else {
157        ExactSubmissionDispositionV1::Resolved
158    }
159}
160
161/// Redacted retained-append item observation. Server error messages are never
162/// persisted in a recovery record.
163#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
164#[serde(deny_unknown_fields)]
165pub struct AppendExactItemResultV1 {
166    pub item_action_id: Digest,
167    #[serde(default, skip_serializing_if = "Option::is_none")]
168    pub locator: Option<Locator>,
169    #[serde(default, skip_serializing_if = "Option::is_none")]
170    pub semantic_digest: Option<Digest>,
171    pub state: RetainedAppendState,
172    pub retry_safety: RetainedAppendRetrySafety,
173    #[serde(default, skip_serializing_if = "Option::is_none")]
174    pub sequence: Option<U64>,
175    #[serde(default, skip_serializing_if = "Option::is_none")]
176    pub primary_term: Option<U64>,
177}
178
179#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
180#[serde(deny_unknown_fields)]
181pub struct AppendExactResultV1 {
182    pub outcome: ExactMutationOutcomeV1,
183    pub batch_id: Digest,
184    pub index: Resource,
185    pub items: Vec<AppendExactItemResultV1>,
186    #[serde(default, skip_serializing_if = "Vec::is_empty")]
187    pub visibility: Vec<RetainedAppendObservationOutcome>,
188}
189
190#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
191#[serde(deny_unknown_fields)]
192pub struct DocumentMutationExactResultV1 {
193    pub outcome: ExactMutationOutcomeV1,
194    pub index: Resource,
195    #[serde(default, skip_serializing_if = "Option::is_none")]
196    pub locator: Option<Locator>,
197    pub state: String,
198    pub submitted: bool,
199    #[serde(default, skip_serializing_if = "Option::is_none")]
200    pub sequence: Option<U64>,
201    #[serde(default, skip_serializing_if = "Option::is_none")]
202    pub mapping_generation: Option<U64>,
203    pub mapping_adopted: bool,
204}
205
206#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
207#[serde(deny_unknown_fields)]
208pub struct ReconcileIndexExactResultV1 {
209    pub outcome: ExactMutationOutcomeV1,
210    pub name: String,
211    #[serde(default, skip_serializing_if = "Option::is_none")]
212    pub locator: Option<Locator>,
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub resource: Option<Resource>,
215    pub state: String,
216}
217
218#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
219#[serde(tag = "kind", content = "result", rename_all = "snake_case", deny_unknown_fields)]
220enum ExactTerminalResultV1 {
221    Append(AppendExactResultV1),
222    Document(DocumentMutationExactResultV1),
223    Index(ReconcileIndexExactResultV1),
224}
225
226#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
227#[serde(tag = "kind", content = "value", rename_all = "snake_case", deny_unknown_fields)]
228enum ExactPreparationV1 {
229    Append(RetainedAppendBulkOutcome),
230    Document {
231        locator: Locator,
232        semantic_digest: Digest,
233    },
234    Index {
235        locator: Locator,
236        expires_at: U64,
237        semantic_digest: Digest,
238        resource_spec: Box<IndexResourceSpec>,
239    },
240}
241
242/// Non-secret state sufficient to resume the same effect. Callers that do not
243/// use the filesystem adapter must persist this value after an unresolved
244/// return.
245#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
246#[serde(deny_unknown_fields)]
247pub struct ExactWorkflowRecoveryV2 {
248    pub format_version: u16,
249    pub revision: u64,
250    pub phase: ExactWorkflowPhaseV2,
251    pub mutation_kind: ExactMutationKindV1,
252    pub request_sha256: Digest,
253    pub cluster_id: Identity,
254    pub collection_id: Identity,
255    #[serde(default, skip_serializing_if = "Option::is_none")]
256    pub index: Option<Resource>,
257    pub caller_action_id: Digest,
258    pub effect_deadline_unix_millis: U64,
259    #[serde(default, skip_serializing_if = "Option::is_none")]
260    pub last_observed_unix_millis: Option<U64>,
261    #[serde(default, skip_serializing_if = "Option::is_none")]
262    preparation: Option<ExactPreparationV1>,
263    #[serde(default, skip_serializing_if = "Option::is_none")]
264    submission_disposition: Option<ExactSubmissionDispositionV1>,
265    #[serde(default, skip_serializing_if = "Option::is_none")]
266    terminal: Option<ExactTerminalResultV1>,
267}
268
269/// One result plus the exact non-secret recovery state observed at return.
270#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
271#[serde(deny_unknown_fields)]
272pub struct ExactExecutionV2<T> {
273    pub result: T,
274    pub recovery: ExactWorkflowRecoveryV2,
275}
276
277/// Boxed future used by the object-safe caller-owned recovery-store boundary.
278pub type ExactWorkflowStoreFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
279
280/// Closed failures returned by a caller-owned exact-workflow store.
281#[derive(Clone, Debug, PartialEq)]
282pub enum ExactWorkflowStoreError {
283    Unavailable,
284    Conflict {
285        observed: Option<Box<ExactWorkflowRecoveryV2>>,
286    },
287    Malformed,
288    UnsupportedFormat {
289        observed: u16,
290    },
291    IntentMismatch,
292    Internal,
293}
294
295impl Display for ExactWorkflowStoreError {
296    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
297        formatter.write_str(match self {
298            Self::Unavailable => "the exact workflow recovery store is unavailable",
299            Self::Conflict { .. } => "the exact workflow recovery store compare-and-exchange conflicted",
300            Self::Malformed => "the exact workflow recovery store returned malformed current state",
301            Self::UnsupportedFormat { .. } => "the exact workflow recovery store returned an unsupported format",
302            Self::IntentMismatch => "the exact workflow recovery store key belongs to another intent",
303            Self::Internal => "the exact workflow recovery store failed internally",
304        })
305    }
306}
307
308impl Error for ExactWorkflowStoreError {}
309
310/// Linearizable durable CAS scoped by the caller to one exact workflow key.
311///
312/// The implementation owns storage mechanics only. Lodestone validates every
313/// loaded value and computes every legal successor before invoking the store.
314pub trait ExactWorkflowRecoveryStore: Send + Sync {
315    fn load(&self) -> ExactWorkflowStoreFuture<'_, Result<Option<ExactWorkflowRecoveryV2>, ExactWorkflowStoreError>>;
316
317    fn compare_exchange(
318        &self,
319        expected_revision: Option<u64>,
320        next: ExactWorkflowRecoveryV2,
321    ) -> ExactWorkflowStoreFuture<'_, Result<(), ExactWorkflowStoreError>>;
322}
323
324/// Filesystem-backed adapter over Lodestone's sealed create-new workflow
325/// journal. The directory is exclusive to one exact request intent.
326#[derive(Clone)]
327pub struct FilesystemExactWorkflowRecoveryStore {
328    path: PathBuf,
329    session: Arc<tokio::sync::Mutex<Option<FilesystemExactWorkflowSession>>>,
330}
331
332struct FilesystemExactWorkflowSession {
333    journal: WorkflowJournal<ExactMutationIntent>,
334    recovery: ExactWorkflowRecoveryV2,
335}
336
337impl fmt::Debug for FilesystemExactWorkflowRecoveryStore {
338    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
339        formatter
340            .debug_struct("FilesystemExactWorkflowRecoveryStore")
341            .field("path", &self.path)
342            .finish()
343    }
344}
345
346impl FilesystemExactWorkflowRecoveryStore {
347    #[must_use]
348    pub fn new(path: impl Into<PathBuf>) -> Self {
349        Self {
350            path: path.into(),
351            session: Arc::new(tokio::sync::Mutex::new(None)),
352        }
353    }
354
355    #[must_use]
356    pub fn path(&self) -> &Path {
357        &self.path
358    }
359}
360
361impl ExactWorkflowRecoveryStore for FilesystemExactWorkflowRecoveryStore {
362    fn load(&self) -> ExactWorkflowStoreFuture<'_, Result<Option<ExactWorkflowRecoveryV2>, ExactWorkflowStoreError>> {
363        Box::pin(async move {
364            let mut session = self.session.lock().await;
365            if let Some(current) = session.as_ref() {
366                return Ok(Some(current.recovery.clone()));
367            }
368            let Some(opened) = open_filesystem_recovery(&self.path).await? else {
369                return Ok(None);
370            };
371            let recovery = opened.recovery.clone();
372            *session = Some(opened);
373            Ok(Some(recovery))
374        })
375    }
376
377    fn compare_exchange(
378        &self,
379        expected_revision: Option<u64>,
380        next: ExactWorkflowRecoveryV2,
381    ) -> ExactWorkflowStoreFuture<'_, Result<(), ExactWorkflowStoreError>> {
382        Box::pin(async move {
383            let mut session = self.session.lock().await;
384            if session.is_none() {
385                *session = open_filesystem_recovery(&self.path).await?;
386            }
387            match (session.as_mut(), expected_revision) {
388                (Some(current), Some(expected)) if current.recovery.revision == expected => {
389                    let transition = transition_between(&current.recovery, &next)?;
390                    current
391                        .journal
392                        .append(transition)
393                        .await
394                        .map_err(map_journal_store_error)?;
395                    current.recovery = next;
396                    Ok(())
397                }
398                (Some(current), _) => Err(ExactWorkflowStoreError::Conflict {
399                    observed: Some(Box::new(current.recovery.clone())),
400                }),
401                (None, Some(_)) => Err(ExactWorkflowStoreError::Conflict { observed: None }),
402                (None, None) => {
403                    validate_initial_recovery(&next)?;
404                    let intent = intent_from_recovery(&next);
405                    let replay = WorkflowJournal::open_or_create(&self.path, intent)
406                        .await
407                        .map_err(map_journal_store_error)?;
408                    let created = replay.was_created();
409                    let opened = filesystem_recovery_from_replay(replay)?;
410                    if !created {
411                        let observed = opened.recovery.clone();
412                        *session = Some(opened);
413                        return Err(ExactWorkflowStoreError::Conflict {
414                            observed: Some(Box::new(observed)),
415                        });
416                    }
417                    if opened.recovery != next {
418                        return Err(ExactWorkflowStoreError::Malformed);
419                    }
420                    *session = Some(opened);
421                    Ok(())
422                }
423            }
424        })
425    }
426}
427
428async fn open_filesystem_recovery(
429    path: &Path,
430) -> Result<Option<FilesystemExactWorkflowSession>, ExactWorkflowStoreError> {
431    let replay = WorkflowJournal::<ExactMutationIntent>::open_existing(path)
432        .await
433        .map_err(map_journal_store_error)?;
434    replay.map(filesystem_recovery_from_replay).transpose()
435}
436
437fn filesystem_recovery_from_replay(
438    replay: crate::WorkflowJournalReplay<ExactMutationIntent>,
439) -> Result<FilesystemExactWorkflowSession, ExactWorkflowStoreError> {
440    let mut recovery = recovery_from_intent(replay.intent());
441    let (journal, transitions) = replay.into_parts();
442    for transition in transitions {
443        reduce_transition(&mut recovery, &transition).map_err(|_| ExactWorkflowStoreError::Malformed)?;
444    }
445    validate_recovery_shape(&recovery).map_err(|_| ExactWorkflowStoreError::Malformed)?;
446    Ok(FilesystemExactWorkflowSession { journal, recovery })
447}
448
449const fn map_journal_store_error(error: WorkflowJournalError) -> ExactWorkflowStoreError {
450    match error {
451        WorkflowJournalError::UnsupportedFormat(observed) => ExactWorkflowStoreError::UnsupportedFormat { observed },
452        WorkflowJournalError::IntentMismatch => ExactWorkflowStoreError::IntentMismatch,
453        WorkflowJournalError::InvalidIntent
454        | WorkflowJournalError::InvalidTransition
455        | WorkflowJournalError::Corrupt
456        | WorkflowJournalError::Codec
457        | WorkflowJournalError::LimitExceeded => ExactWorkflowStoreError::Malformed,
458        WorkflowJournalError::UnsupportedPlatform
459        | WorkflowJournalError::InvalidPath
460        | WorkflowJournalError::InsecurePath
461        | WorkflowJournalError::Busy
462        | WorkflowJournalError::Io
463        | WorkflowJournalError::RandomUnavailable
464        | WorkflowJournalError::ReopenRequired
465        | WorkflowJournalError::WorkerUnavailable => ExactWorkflowStoreError::Unavailable,
466    }
467}
468
469/// Injectable wall clock. Time is a liveness budget, never ordering or commit
470/// evidence.
471pub trait ExactWorkflowClock: Send + Sync {
472    fn unix_millis(&self) -> Result<u64, ExactWorkflowError>;
473}
474
475#[derive(Debug)]
476struct SystemExactWorkflowClock;
477
478impl ExactWorkflowClock for SystemExactWorkflowClock {
479    fn unix_millis(&self) -> Result<u64, ExactWorkflowError> {
480        let duration = SystemTime::now()
481            .duration_since(UNIX_EPOCH)
482            .map_err(|_| ExactWorkflowError::ClockUnavailable)?;
483        u64::try_from(duration.as_millis()).map_err(|_| ExactWorkflowError::ClockUnavailable)
484    }
485}
486
487/// Immutable budgets and optional recovery storage for one exact invocation.
488#[derive(Clone)]
489pub struct ExactWorkflowOptions {
490    effect_deadline_unix_millis: u64,
491    observation_deadline_unix_millis: Option<u64>,
492    store: Option<Arc<dyn ExactWorkflowRecoveryStore>>,
493    recovery: Option<ExactWorkflowRecoveryV2>,
494    clock: Arc<dyn ExactWorkflowClock>,
495    cancellation: CallCancellation,
496    store_conflict_limit: u32,
497}
498
499impl ExactWorkflowOptions {
500    #[must_use]
501    pub fn new(effect_deadline_unix_millis: u64) -> Self {
502        Self {
503            effect_deadline_unix_millis,
504            observation_deadline_unix_millis: None,
505            store: None,
506            recovery: None,
507            clock: Arc::new(SystemExactWorkflowClock),
508            cancellation: CallCancellation::new(),
509            store_conflict_limit: DEFAULT_EXACT_WORKFLOW_STORE_CONFLICT_LIMIT,
510        }
511    }
512
513    #[must_use]
514    pub fn with_observation_deadline_unix_millis(mut self, value: u64) -> Self {
515        self.observation_deadline_unix_millis = Some(value);
516        self
517    }
518
519    #[must_use]
520    pub fn with_recovery_store(mut self, store: Arc<dyn ExactWorkflowRecoveryStore>) -> Self {
521        self.store = Some(store);
522        self
523    }
524
525    #[must_use]
526    pub fn with_store_conflict_limit(mut self, value: u32) -> Self {
527        self.store_conflict_limit = value;
528        self
529    }
530
531    #[must_use]
532    pub fn with_recovery(mut self, recovery: ExactWorkflowRecoveryV2) -> Self {
533        self.recovery = Some(recovery);
534        self
535    }
536
537    #[must_use]
538    pub fn with_clock(mut self, clock: Arc<dyn ExactWorkflowClock>) -> Self {
539        self.clock = clock;
540        self
541    }
542
543    #[must_use]
544    pub fn with_cancellation(mut self, cancellation: CallCancellation) -> Self {
545        self.cancellation = cancellation;
546        self
547    }
548}
549
550#[derive(Debug)]
551pub enum ExactWorkflowError {
552    InvalidInput,
553    InvalidRecovery,
554    RequestCodec,
555    CoreShape,
556    ClockUnavailable,
557    Journal(WorkflowJournalError),
558    Store {
559        source: ExactWorkflowStoreError,
560        last_durable_recovery: Option<Box<ExactWorkflowRecoveryV2>>,
561    },
562    StoreConflictBudgetExhausted {
563        last_durable_recovery: Box<ExactWorkflowRecoveryV2>,
564    },
565    Cancelled {
566        last_durable_recovery: Box<ExactWorkflowRecoveryV2>,
567    },
568    Client(ClientError),
569}
570
571impl Display for ExactWorkflowError {
572    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
573        formatter.write_str(match self {
574            Self::InvalidInput => "the exact mutation input is invalid",
575            Self::InvalidRecovery => "the exact mutation recovery state does not match this request",
576            Self::RequestCodec => "the exact mutation request cannot be canonically encoded",
577            Self::CoreShape => "Core changed an exact mutation identity or outcome shape",
578            Self::ClockUnavailable => "the exact mutation wall clock is unavailable",
579            Self::Journal(_) => "the exact mutation journal could not be verified or updated",
580            Self::Store { .. } => "the exact mutation recovery store failed",
581            Self::StoreConflictBudgetExhausted { .. } => {
582                "the exact mutation recovery store conflict budget was exhausted"
583            }
584            Self::Cancelled { .. } => "the exact mutation workflow was cancelled",
585            Self::Client(_) => "the exact mutation client call failed",
586        })
587    }
588}
589
590impl Error for ExactWorkflowError {}
591
592impl ExactWorkflowError {
593    #[must_use]
594    pub fn last_durable_recovery(&self) -> Option<&ExactWorkflowRecoveryV2> {
595        match self {
596            Self::Store {
597                last_durable_recovery, ..
598            } => last_durable_recovery.as_deref(),
599            Self::StoreConflictBudgetExhausted { last_durable_recovery }
600            | Self::Cancelled { last_durable_recovery } => Some(last_durable_recovery),
601            _ => None,
602        }
603    }
604}
605
606impl From<WorkflowJournalError> for ExactWorkflowError {
607    fn from(value: WorkflowJournalError) -> Self {
608        Self::Journal(value)
609    }
610}
611
612/// Append request plus the optional visibility observation required by the
613/// caller. Its canonical encoding is bound into recovery state.
614#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
615#[serde(deny_unknown_fields)]
616pub struct AppendExactRequestV1 {
617    pub batch_id: Digest,
618    pub index: Resource,
619    pub items: Vec<RetainedAppendItem>,
620    #[serde(default)]
621    pub observe_committed: bool,
622}
623
624#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
625#[serde(deny_unknown_fields)]
626struct ExactMutationIntent {
627    format_version: u16,
628    mutation_kind: ExactMutationKindV1,
629    request_sha256: Digest,
630    cluster_id: Identity,
631    collection_id: Identity,
632    index: Option<Resource>,
633    caller_action_id: Digest,
634    effect_deadline_unix_millis: U64,
635}
636
637impl sealed::Sealed for ExactMutationIntent {}
638
639impl WorkflowJournalIntent for ExactMutationIntent {
640    type Transition = ExactMutationTransition;
641
642    const KIND: LocalOperatorWorkflowKind = LocalOperatorWorkflowKind::ExactMutation;
643
644    fn validate(&self) -> Result<(), WorkflowJournalError> {
645        if self.format_version != EXACT_MUTATION_FORMAT_VERSION
646            || self.request_sha256.validate().is_err()
647            || self.cluster_id.validate().is_err()
648            || self.collection_id.validate().is_err()
649            || self.caller_action_id.validate().is_err()
650            || parse_u64(&self.effect_deadline_unix_millis).is_none_or(|value| value == 0)
651            || self.index.as_ref().is_some_and(|value| !valid_index(value))
652        {
653            return Err(WorkflowJournalError::InvalidIntent);
654        }
655        Ok(())
656    }
657}
658
659#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
660#[serde(tag = "transition", rename_all = "snake_case", deny_unknown_fields)]
661enum ExactMutationTransition {
662    ClockObserved { unix_millis: U64 },
663    Prepared { preparation: ExactPreparationV1 },
664    SubmissionStarted,
665    SubmissionObserved { disposition: ExactSubmissionDispositionV1 },
666    Terminal { result: ExactTerminalResultV1 },
667}
668
669impl sealed::Sealed for ExactMutationTransition {}
670
671impl WorkflowJournalTransition for ExactMutationTransition {
672    fn validate(&self) -> Result<(), WorkflowJournalError> {
673        match self {
674            Self::ClockObserved { unix_millis } if parse_u64(unix_millis).is_some() => Ok(()),
675            Self::Prepared { preparation } if valid_preparation(preparation) => Ok(()),
676            Self::SubmissionStarted => Ok(()),
677            Self::SubmissionObserved { .. } => Ok(()),
678            Self::Terminal { result } if valid_terminal(result) => Ok(()),
679            _ => Err(WorkflowJournalError::InvalidTransition),
680        }
681    }
682}
683
684enum ExactSession {
685    Durable {
686        store: Arc<dyn ExactWorkflowRecoveryStore>,
687        conflict_limit: u32,
688        deadline_unix_millis: u64,
689        clock: Arc<dyn ExactWorkflowClock>,
690        cancellation: CallCancellation,
691    },
692    Ephemeral,
693}
694
695impl ExactSession {
696    async fn record(
697        &self,
698        state: &mut ExactWorkflowRecoveryV2,
699        transition: ExactMutationTransition,
700    ) -> Result<bool, ExactWorkflowError> {
701        let Self::Durable {
702            store,
703            conflict_limit,
704            deadline_unix_millis,
705            clock,
706            cancellation,
707        } = self
708        else {
709            reduce_transition(state, &transition)?;
710            return Ok(true);
711        };
712        let mut conflicts = 0_u32;
713        loop {
714            if cancellation.is_cancelled() {
715                return Err(ExactWorkflowError::Cancelled {
716                    last_durable_recovery: Box::new(state.clone()),
717                });
718            }
719            if transition_is_reflected(state, &transition) {
720                return Ok(false);
721            }
722            if conflicts > 0 && clock.unix_millis()? >= *deadline_unix_millis {
723                return Err(ExactWorkflowError::StoreConflictBudgetExhausted {
724                    last_durable_recovery: Box::new(state.clone()),
725                });
726            }
727            let mut next = state.clone();
728            reduce_transition(&mut next, &transition)?;
729            match store.compare_exchange(Some(state.revision), next.clone()).await {
730                Ok(()) => {
731                    *state = next;
732                    return Ok(true);
733                }
734                Err(ExactWorkflowStoreError::Conflict {
735                    observed: Some(observed),
736                }) => {
737                    let observed = *observed;
738                    validate_recovery_descendant(state, &observed)?;
739                    *state = observed;
740                    if transition_is_reflected(state, &transition) {
741                        return Ok(false);
742                    }
743                    conflicts = conflicts.saturating_add(1);
744                    if conflicts > *conflict_limit {
745                        return Err(ExactWorkflowError::StoreConflictBudgetExhausted {
746                            last_durable_recovery: Box::new(state.clone()),
747                        });
748                    }
749                }
750                Err(ExactWorkflowStoreError::Conflict { observed: None }) => {
751                    return Err(store_failure(ExactWorkflowStoreError::Malformed, Some(state.clone())));
752                }
753                Err(error) => return Err(store_failure(error, Some(state.clone()))),
754            }
755        }
756    }
757}
758
759impl Client {
760    /// Runs retained append through prepare, submit, and resolver without ever
761    /// replacing the caller's batch or item action identities.
762    pub async fn append_exact(
763        &self,
764        request: AppendExactRequestV1,
765        options: ExactWorkflowOptions,
766    ) -> Result<ExactExecutionV2<AppendExactResultV1>, ExactWorkflowError> {
767        validate_append_request(&request)?;
768        let request_sha256 = canonical_sha256(&request)?;
769        let (cluster_id, collection_id) = self.exact_profile_scope();
770        let intent = exact_intent(
771            ExactMutationKindV1::Append,
772            request_sha256,
773            cluster_id,
774            collection_id,
775            Some(request.index.clone()),
776            request.batch_id.clone(),
777            options.effect_deadline_unix_millis,
778        );
779        let (session, mut recovery) = open_session(&intent, &options).await?;
780        let mut options = options;
781        options.effect_deadline_unix_millis = recovery_effect_deadline(&recovery)?;
782        if let Some(ExactTerminalResultV1::Append(result)) = &recovery.terminal {
783            return Ok(execution(result.clone(), recovery));
784        }
785        let now = observe_clock(&session, &mut recovery, &options).await?;
786        if let Some(ExactTerminalResultV1::Append(result)) = &recovery.terminal {
787            return Ok(execution(result.clone(), recovery));
788        }
789        if clock_regressed(&recovery, now) {
790            return Ok(execution(
791                append_status(&request, &recovery, ExactMutationOutcomeV1::ClockRegression),
792                recovery,
793            ));
794        }
795
796        if recovery.preparation.is_none() {
797            if now >= options.effect_deadline_unix_millis {
798                let result = append_status(&request, &recovery, ExactMutationOutcomeV1::EffectDeadlineExpired);
799                return finish_append(&session, &mut recovery, result).await;
800            }
801            let prepared = self
802                .prepare_append_bulk(
803                    PrepareAppendBulkRequest {
804                        batch_id: request.batch_id.clone(),
805                        index: request.index.clone(),
806                        items: request.items.clone(),
807                    },
808                    effect_options(now, options.effect_deadline_unix_millis, &options)?,
809                )
810                .await
811                .map_err(ExactWorkflowError::Client)?
812                .outcome;
813            validate_append_outcome(&request, &prepared, true)?;
814            session
815                .record(
816                    &mut recovery,
817                    ExactMutationTransition::Prepared {
818                        preparation: ExactPreparationV1::Append(redact_append_outcome(prepared)),
819                    },
820                )
821                .await?;
822            if let Some(ExactTerminalResultV1::Append(result)) = &recovery.terminal {
823                return Ok(execution(result.clone(), recovery));
824            }
825        }
826
827        let preparations = append_preparation(&recovery)?.items.clone();
828        let may_submit = recovery.phase == ExactWorkflowPhaseV2::Prepared
829            || recovery.submission_disposition == Some(ExactSubmissionDispositionV1::NotSubmitted);
830        if may_submit && now < options.effect_deadline_unix_millis {
831            let owns_submission = session
832                .record(&mut recovery, ExactMutationTransition::SubmissionStarted)
833                .await?;
834            if let Some(ExactTerminalResultV1::Append(result)) = &recovery.terminal {
835                return Ok(execution(result.clone(), recovery));
836            }
837            if owns_submission {
838                match self
839                    .submit_append_bulk(
840                        SubmitAppendBulkRequest {
841                            batch_id: request.batch_id.clone(),
842                            index: request.index.clone(),
843                            items: request.items.clone(),
844                            preparations,
845                        },
846                        effect_options(now, options.effect_deadline_unix_millis, &options)?,
847                    )
848                    .await
849                {
850                    Ok(submitted) => {
851                        validate_append_outcome(&request, &submitted.outcome, false)?;
852                        let disposition = if submitted.outcome.items.iter().any(|item| {
853                            matches!(
854                                item.state,
855                                RetainedAppendState::Committed
856                                    | RetainedAppendState::Replayed
857                                    | RetainedAppendState::SubmittedIndeterminate
858                            )
859                        }) {
860                            ExactSubmissionDispositionV1::Submitted
861                        } else {
862                            ExactSubmissionDispositionV1::NotSubmitted
863                        };
864                        let published = session
865                            .record(
866                                &mut recovery,
867                                ExactMutationTransition::SubmissionObserved { disposition },
868                            )
869                            .await?;
870                        if let Some(ExactTerminalResultV1::Append(result)) = &recovery.terminal {
871                            return Ok(execution(result.clone(), recovery));
872                        }
873                        if published
874                            && let Some(mut result) = append_terminal_result(&request, &submitted.outcome, false)
875                        {
876                            if !terminal_outcome(result.outcome) {
877                                return Ok(execution(result, recovery));
878                            }
879                            if request.observe_committed
880                                && matches!(
881                                    result.outcome,
882                                    ExactMutationOutcomeV1::Applied | ExactMutationOutcomeV1::Resolved
883                                )
884                            {
885                                let Some(observe_options) = resolution_options(now, &options)? else {
886                                    result.outcome = ExactMutationOutcomeV1::Indeterminate;
887                                    return Ok(execution(result, recovery));
888                                };
889                                result = observe_append_visibility(self, &request, result, observe_options).await?;
890                                if result.outcome == ExactMutationOutcomeV1::Indeterminate {
891                                    return Ok(execution(result, recovery));
892                                }
893                            }
894                            return finish_append(&session, &mut recovery, result).await;
895                        }
896                    }
897                    Err(error) => {
898                        let disposition = ExactSubmissionDispositionV1::from(error.submission_disposition());
899                        session
900                            .record(
901                                &mut recovery,
902                                ExactMutationTransition::SubmissionObserved { disposition },
903                            )
904                            .await?;
905                        if let Some(ExactTerminalResultV1::Append(result)) = &recovery.terminal {
906                            return Ok(execution(result.clone(), recovery));
907                        }
908                        if recovery.submission_disposition == Some(ExactSubmissionDispositionV1::NotSubmitted) {
909                            return Ok(execution(
910                                append_status(&request, &recovery, ExactMutationOutcomeV1::NotSubmitted),
911                                recovery,
912                            ));
913                        }
914                    }
915                }
916            }
917        }
918
919        let Some(resolve_options) = resolution_options(now, &options)? else {
920            return Ok(execution(
921                append_status(&request, &recovery, ExactMutationOutcomeV1::Indeterminate),
922                recovery,
923            ));
924        };
925        let resolved = self
926            .resolve_append_bulk(
927                ResolveAppendBulkRequest {
928                    batch_id: request.batch_id.clone(),
929                    index: request.index.clone(),
930                    items: append_preparation(&recovery)?.items.clone(),
931                },
932                resolve_options,
933            )
934            .await;
935        let outcome = match resolved {
936            Ok(value) => value.outcome,
937            Err(_) => {
938                return Ok(execution(
939                    append_status(&request, &recovery, ExactMutationOutcomeV1::Indeterminate),
940                    recovery,
941                ));
942            }
943        };
944        validate_append_outcome(&request, &outcome, false)?;
945        record_resolution_observation(&session, &mut recovery, append_resolution_disposition(&outcome)).await?;
946        if let Some(ExactTerminalResultV1::Append(result)) = &recovery.terminal {
947            return Ok(execution(result.clone(), recovery));
948        }
949        let Some(mut result) = append_terminal_result(&request, &outcome, true) else {
950            return Ok(execution(
951                append_result(&request, &outcome, ExactMutationOutcomeV1::Indeterminate),
952                recovery,
953            ));
954        };
955        if request.observe_committed
956            && matches!(
957                result.outcome,
958                ExactMutationOutcomeV1::Applied | ExactMutationOutcomeV1::Resolved
959            )
960        {
961            let Some(observe_options) = resolution_options(now, &options)? else {
962                result.outcome = ExactMutationOutcomeV1::Indeterminate;
963                return Ok(execution(result, recovery));
964            };
965            result = observe_append_visibility(self, &request, result, observe_options).await?;
966            if result.outcome == ExactMutationOutcomeV1::Indeterminate {
967                return Ok(execution(result, recovery));
968            }
969        }
970        finish_append(&session, &mut recovery, result).await
971    }
972
973    /// Runs one document mutation with a stable prepared locator and resolves
974    /// any possibly submitted result before returning a terminal claim.
975    pub async fn mutate_document_exact(
976        &self,
977        request: PrepareDocumentMutationRequest,
978        options: ExactWorkflowOptions,
979    ) -> Result<ExactExecutionV2<DocumentMutationExactResultV1>, ExactWorkflowError> {
980        if !valid_index(&request.index) || request.document_id.is_empty() {
981            return Err(ExactWorkflowError::InvalidInput);
982        }
983        let request_sha256 = canonical_sha256(&request)?;
984        let (cluster_id, collection_id) = self.exact_profile_scope();
985        let intent = exact_intent(
986            ExactMutationKindV1::Document,
987            request_sha256.clone(),
988            cluster_id,
989            collection_id,
990            Some(request.index.clone()),
991            request_sha256,
992            options.effect_deadline_unix_millis,
993        );
994        let (session, mut recovery) = open_session(&intent, &options).await?;
995        let mut options = options;
996        options.effect_deadline_unix_millis = recovery_effect_deadline(&recovery)?;
997        if let Some(ExactTerminalResultV1::Document(result)) = &recovery.terminal {
998            return Ok(execution(result.clone(), recovery));
999        }
1000        let now = observe_clock(&session, &mut recovery, &options).await?;
1001        if let Some(ExactTerminalResultV1::Document(result)) = &recovery.terminal {
1002            return Ok(execution(result.clone(), recovery));
1003        }
1004        if clock_regressed(&recovery, now) {
1005            return Ok(execution(
1006                document_status(&request, &recovery, ExactMutationOutcomeV1::ClockRegression),
1007                recovery,
1008            ));
1009        }
1010        if recovery.preparation.is_none() {
1011            if now >= options.effect_deadline_unix_millis {
1012                let result = document_status(&request, &recovery, ExactMutationOutcomeV1::EffectDeadlineExpired);
1013                return finish_document(&session, &mut recovery, result).await;
1014            }
1015            let prepared = self
1016                .prepare_document_mutation(
1017                    request.clone(),
1018                    effect_options(now, options.effect_deadline_unix_millis, &options)?,
1019                )
1020                .await
1021                .map_err(ExactWorkflowError::Client)?;
1022            if prepared.locator.authority.is_empty() || prepared.semantic_digest.validate().is_err() {
1023                return Err(ExactWorkflowError::CoreShape);
1024            }
1025            session
1026                .record(
1027                    &mut recovery,
1028                    ExactMutationTransition::Prepared {
1029                        preparation: ExactPreparationV1::Document {
1030                            locator: prepared.locator,
1031                            semantic_digest: prepared.semantic_digest,
1032                        },
1033                    },
1034                )
1035                .await?;
1036            if let Some(ExactTerminalResultV1::Document(result)) = &recovery.terminal {
1037                return Ok(execution(result.clone(), recovery));
1038            }
1039        }
1040        let (locator, semantic_digest) = document_preparation(&recovery)?;
1041        let may_submit = recovery.phase == ExactWorkflowPhaseV2::Prepared
1042            || recovery.submission_disposition == Some(ExactSubmissionDispositionV1::NotSubmitted);
1043        if may_submit && now < options.effect_deadline_unix_millis {
1044            let owns_submission = session
1045                .record(&mut recovery, ExactMutationTransition::SubmissionStarted)
1046                .await?;
1047            if let Some(ExactTerminalResultV1::Document(result)) = &recovery.terminal {
1048                return Ok(execution(result.clone(), recovery));
1049            }
1050            if owns_submission {
1051                match self
1052                    .submit_document_mutation(
1053                        SubmitDocumentMutationRequest {
1054                            locator: locator.clone(),
1055                            semantic_digest,
1056                            index: request.index.clone(),
1057                            document_id: request.document_id.clone(),
1058                            operation: request.operation.clone(),
1059                            source: request.source.clone(),
1060                            record: request.record.clone(),
1061                            routing: request.routing.clone(),
1062                            condition: request.condition.clone(),
1063                            pipeline: request.pipeline.clone(),
1064                        },
1065                        effect_options(now, options.effect_deadline_unix_millis, &options)?,
1066                    )
1067                    .await
1068                {
1069                    Ok(value) => {
1070                        if value.locator != locator {
1071                            return Err(ExactWorkflowError::CoreShape);
1072                        }
1073                        let published = session
1074                            .record(
1075                                &mut recovery,
1076                                ExactMutationTransition::SubmissionObserved {
1077                                    disposition: exact_submission_disposition(&value.mutation_outcome),
1078                                },
1079                            )
1080                            .await?;
1081                        if let Some(ExactTerminalResultV1::Document(result)) = &recovery.terminal {
1082                            return Ok(execution(result.clone(), recovery));
1083                        }
1084                        if published
1085                            && matches!(&value.mutation_outcome, MutationOutcome::Terminal)
1086                            && matches!(value.state.as_str(), "applied" | "dropped")
1087                        {
1088                            let submitted = value.state == "applied";
1089                            return finish_document(
1090                                &session,
1091                                &mut recovery,
1092                                DocumentMutationExactResultV1 {
1093                                    outcome: ExactMutationOutcomeV1::Applied,
1094                                    index: request.index,
1095                                    locator: Some(value.locator),
1096                                    state: value.state,
1097                                    submitted,
1098                                    sequence: value.sequence,
1099                                    mapping_generation: value.mapping_generation,
1100                                    mapping_adopted: value.mapping_adopted,
1101                                },
1102                            )
1103                            .await;
1104                        }
1105                    }
1106                    Err(error) => {
1107                        let disposition = ExactSubmissionDispositionV1::from(error.submission_disposition());
1108                        session
1109                            .record(
1110                                &mut recovery,
1111                                ExactMutationTransition::SubmissionObserved { disposition },
1112                            )
1113                            .await?;
1114                        if let Some(ExactTerminalResultV1::Document(result)) = &recovery.terminal {
1115                            return Ok(execution(result.clone(), recovery));
1116                        }
1117                        if recovery.submission_disposition == Some(ExactSubmissionDispositionV1::NotSubmitted) {
1118                            return Ok(execution(
1119                                document_status(&request, &recovery, ExactMutationOutcomeV1::NotSubmitted),
1120                                recovery,
1121                            ));
1122                        }
1123                    }
1124                }
1125            }
1126        }
1127        let Some(resolve_options) = resolution_options(now, &options)? else {
1128            return Ok(execution(
1129                document_status(&request, &recovery, ExactMutationOutcomeV1::Indeterminate),
1130                recovery,
1131            ));
1132        };
1133        let resolve_options = resolve_options
1134            .with_index_name(request.index.name.clone())
1135            .map_err(ExactWorkflowError::Client)?;
1136        let value = match self
1137            .resolve_document_mutation(
1138                ResolveDocumentMutationRequest {
1139                    locator: locator.clone(),
1140                },
1141                resolve_options,
1142            )
1143            .await
1144        {
1145            Ok(value) => value,
1146            Err(_) => {
1147                return Ok(execution(
1148                    document_status(&request, &recovery, ExactMutationOutcomeV1::Indeterminate),
1149                    recovery,
1150                ));
1151            }
1152        };
1153        if value.locator != locator || value.source.is_some() {
1154            return Err(ExactWorkflowError::CoreShape);
1155        }
1156        let outcome = match value.state.as_str() {
1157            "applied" => ExactMutationOutcomeV1::Resolved,
1158            "failed" => ExactMutationOutcomeV1::Resolved,
1159            "expired_unknown" => ExactMutationOutcomeV1::KnownRetired,
1160            "prepared" => ExactMutationOutcomeV1::NotSubmitted,
1161            "pending" => ExactMutationOutcomeV1::Indeterminate,
1162            _ => return Err(ExactWorkflowError::CoreShape),
1163        };
1164        record_resolution_observation(&session, &mut recovery, resolution_disposition(outcome)).await?;
1165        if let Some(ExactTerminalResultV1::Document(result)) = &recovery.terminal {
1166            return Ok(execution(result.clone(), recovery));
1167        }
1168        let result = DocumentMutationExactResultV1 {
1169            outcome,
1170            index: request.index,
1171            locator: Some(value.locator),
1172            state: value.state,
1173            submitted: !matches!(outcome, ExactMutationOutcomeV1::NotSubmitted),
1174            sequence: value.sequence,
1175            mapping_generation: value.mapping_generation,
1176            mapping_adopted: value.mapping_adopted,
1177        };
1178        if matches!(
1179            outcome,
1180            ExactMutationOutcomeV1::Indeterminate | ExactMutationOutcomeV1::NotSubmitted
1181        ) {
1182            Ok(execution(result, recovery))
1183        } else {
1184            finish_document(&session, &mut recovery, result).await
1185        }
1186    }
1187
1188    /// Reconciles only an absent index to one exact desired definition. An
1189    /// existing different definition is a conflict; this helper never updates.
1190    pub async fn reconcile_index_exact(
1191        &self,
1192        request: PrepareCreateIndexRequest,
1193        options: ExactWorkflowOptions,
1194    ) -> Result<ExactExecutionV2<ReconcileIndexExactResultV1>, ExactWorkflowError> {
1195        if request.name.is_empty() || request.resource_spec.is_none() {
1196            return Err(ExactWorkflowError::InvalidInput);
1197        }
1198        let request_sha256 = canonical_sha256(&request)?;
1199        let (cluster_id, collection_id) = self.exact_profile_scope();
1200        let intent = exact_intent(
1201            ExactMutationKindV1::Index,
1202            request_sha256.clone(),
1203            cluster_id,
1204            collection_id,
1205            None,
1206            request_sha256,
1207            options.effect_deadline_unix_millis,
1208        );
1209        let (session, mut recovery) = open_session(&intent, &options).await?;
1210        let mut options = options;
1211        options.effect_deadline_unix_millis = recovery_effect_deadline(&recovery)?;
1212        if let Some(ExactTerminalResultV1::Index(result)) = &recovery.terminal {
1213            return Ok(execution(result.clone(), recovery));
1214        }
1215        let now = observe_clock(&session, &mut recovery, &options).await?;
1216        if let Some(ExactTerminalResultV1::Index(result)) = &recovery.terminal {
1217            return Ok(execution(result.clone(), recovery));
1218        }
1219        if clock_regressed(&recovery, now) {
1220            return Ok(execution(
1221                index_status(&request, &recovery, ExactMutationOutcomeV1::ClockRegression),
1222                recovery,
1223            ));
1224        }
1225
1226        if recovery.preparation.is_none() {
1227            let Some(lookup_options) = resolution_options(now, &options)? else {
1228                let result = index_status(&request, &recovery, ExactMutationOutcomeV1::EffectDeadlineExpired);
1229                return finish_index(&session, &mut recovery, result).await;
1230            };
1231            let lookup = self
1232                .get_index(
1233                    GetIndexRequest {
1234                        name: request.name.clone(),
1235                        expected_incarnation: None,
1236                    },
1237                    lookup_options,
1238                )
1239                .await;
1240            match lookup {
1241                Ok(existing) => {
1242                    let exact = existing.resource.current_mapping == request.mapping
1243                        && request.resource_spec.as_ref() == Some(&existing.resource.resource_spec)
1244                        && existing.resource.summary.retained_event_identity
1245                            == request.retained_event_identity.unwrap_or(false);
1246                    let outcome = if exact && existing.resource.summary.state == "active" {
1247                        ExactMutationOutcomeV1::AlreadySatisfied
1248                    } else if exact {
1249                        ExactMutationOutcomeV1::Indeterminate
1250                    } else {
1251                        ExactMutationOutcomeV1::Conflict
1252                    };
1253                    let result = ReconcileIndexExactResultV1 {
1254                        outcome,
1255                        name: request.name,
1256                        locator: None,
1257                        resource: Some(Resource {
1258                            kind: "index".to_owned(),
1259                            name: existing.resource.summary.name,
1260                            incarnation: existing.resource.summary.incarnation,
1261                        }),
1262                        state: existing.resource.summary.state,
1263                    };
1264                    if outcome == ExactMutationOutcomeV1::Indeterminate {
1265                        return Ok(execution(result, recovery));
1266                    }
1267                    return finish_index(&session, &mut recovery, result).await;
1268                }
1269                Err(ClientError::Server(error)) if error.code == ErrorCode::NotFound => {}
1270                Err(error) => return Err(ExactWorkflowError::Client(error)),
1271            }
1272            if now >= options.effect_deadline_unix_millis {
1273                let result = index_status(&request, &recovery, ExactMutationOutcomeV1::EffectDeadlineExpired);
1274                return finish_index(&session, &mut recovery, result).await;
1275            }
1276            let prepared = self
1277                .prepare_create_index(
1278                    request.clone(),
1279                    effect_options(now, options.effect_deadline_unix_millis, &options)?,
1280                )
1281                .await
1282                .map_err(ExactWorkflowError::Client)?;
1283            if prepared.locator.authority.is_empty()
1284                || prepared.semantic_digest.validate().is_err()
1285                || prepared.resource_spec != *request.resource_spec.as_ref().ok_or(ExactWorkflowError::InvalidInput)?
1286            {
1287                return Err(ExactWorkflowError::CoreShape);
1288            }
1289            session
1290                .record(
1291                    &mut recovery,
1292                    ExactMutationTransition::Prepared {
1293                        preparation: ExactPreparationV1::Index {
1294                            locator: prepared.locator,
1295                            expires_at: prepared.expires_at,
1296                            semantic_digest: prepared.semantic_digest,
1297                            resource_spec: Box::new(prepared.resource_spec),
1298                        },
1299                    },
1300                )
1301                .await?;
1302            if let Some(ExactTerminalResultV1::Index(result)) = &recovery.terminal {
1303                return Ok(execution(result.clone(), recovery));
1304            }
1305        }
1306        let (locator, _expires_at, semantic_digest, resource_spec) = index_preparation(&recovery)?;
1307        // INVARIANT: preparation expiry belongs to the catalog authority's
1308        // logical clock. The caller clock owns only its immutable effect
1309        // deadline; the authority alone decides whether this preparation is
1310        // still live when submission reaches it.
1311        let submit_deadline = options.effect_deadline_unix_millis;
1312        let submission = SubmitCatalogActionRequest {
1313            locator: locator.clone(),
1314            semantic_digest,
1315            action_kind: create_index_action_kind(request.retained_event_identity).to_owned(),
1316            name: request.name.clone(),
1317            mapping: Some(request.mapping.clone()),
1318            resource_spec: Some(resource_spec),
1319            expected_incarnation: None,
1320        };
1321        if may_start_catalog_submission(&recovery, now, submit_deadline) {
1322            let owns_submission = session
1323                .record(&mut recovery, ExactMutationTransition::SubmissionStarted)
1324                .await?;
1325            if let Some(ExactTerminalResultV1::Index(result)) = &recovery.terminal {
1326                return Ok(execution(result.clone(), recovery));
1327            }
1328            if owns_submission {
1329                match self
1330                    .submit_catalog_action(submission.clone(), effect_options(now, submit_deadline, &options)?)
1331                    .await
1332                {
1333                    Ok(value) => {
1334                        if value.locator != locator {
1335                            return Err(ExactWorkflowError::CoreShape);
1336                        }
1337                        session
1338                            .record(
1339                                &mut recovery,
1340                                ExactMutationTransition::SubmissionObserved {
1341                                    disposition: exact_submission_disposition(&value.mutation_outcome),
1342                                },
1343                            )
1344                            .await?;
1345                        if let Some(ExactTerminalResultV1::Index(result)) = &recovery.terminal {
1346                            return Ok(execution(result.clone(), recovery));
1347                        }
1348                    }
1349                    Err(error) => {
1350                        let disposition = ExactSubmissionDispositionV1::from(error.submission_disposition());
1351                        session
1352                            .record(
1353                                &mut recovery,
1354                                ExactMutationTransition::SubmissionObserved { disposition },
1355                            )
1356                            .await?;
1357                        if let Some(ExactTerminalResultV1::Index(result)) = &recovery.terminal {
1358                            return Ok(execution(result.clone(), recovery));
1359                        }
1360                        if recovery.submission_disposition == Some(ExactSubmissionDispositionV1::NotSubmitted) {
1361                            return Ok(execution(
1362                                index_status(&request, &recovery, ExactMutationOutcomeV1::NotSubmitted),
1363                                recovery,
1364                            ));
1365                        }
1366                    }
1367                }
1368            }
1369        }
1370        let Some(resolve_options) = resolution_options(now, &options)? else {
1371            return Ok(execution(
1372                index_status(&request, &recovery, ExactMutationOutcomeV1::Indeterminate),
1373                recovery,
1374            ));
1375        };
1376        let resolve_options = resolve_options
1377            .with_index_name(request.name.clone())
1378            .map_err(ExactWorkflowError::Client)?;
1379        let mut value = match self
1380            .resolve_catalog_action(
1381                ResolveCatalogActionRequest {
1382                    locator: locator.clone(),
1383                },
1384                resolve_options.clone(),
1385            )
1386            .await
1387        {
1388            Ok(value) => value,
1389            Err(_) => {
1390                return Ok(execution(
1391                    index_status(&request, &recovery, ExactMutationOutcomeV1::Indeterminate),
1392                    recovery,
1393                ));
1394            }
1395        };
1396        if value.locator != locator {
1397            return Err(ExactWorkflowError::CoreShape);
1398        }
1399        let exact_replay =
1400            catalog_resolution_requires_exact_replay(value.state.as_str(), recovery.submission_disposition);
1401        if exact_replay {
1402            // The owner has proved that this exact effect is already durably
1403            // admitted. Calling the same endpoint resumes resolution; it does
1404            // not authorize a first submission or extend the effect identity.
1405            match self.submit_catalog_action(submission, resolve_options.clone()).await {
1406                Ok(replayed) if replayed.locator == locator => {}
1407                Ok(_) => return Err(ExactWorkflowError::CoreShape),
1408                Err(error) if error.submission_disposition() == SubmissionDisposition::NotSubmitted => {
1409                    return Ok(execution(
1410                        index_status(&request, &recovery, ExactMutationOutcomeV1::NotSubmitted),
1411                        recovery,
1412                    ));
1413                }
1414                Err(_) => {
1415                    return Ok(execution(
1416                        index_status(&request, &recovery, ExactMutationOutcomeV1::Indeterminate),
1417                        recovery,
1418                    ));
1419                }
1420            }
1421            value = match self
1422                .resolve_catalog_action(
1423                    ResolveCatalogActionRequest {
1424                        locator: locator.clone(),
1425                    },
1426                    resolve_options,
1427                )
1428                .await
1429            {
1430                Ok(value) => value,
1431                Err(_) => {
1432                    return Ok(execution(
1433                        index_status(&request, &recovery, ExactMutationOutcomeV1::Indeterminate),
1434                        recovery,
1435                    ));
1436                }
1437            };
1438            if value.locator != locator {
1439                return Err(ExactWorkflowError::CoreShape);
1440            }
1441        }
1442        let outcome = match value.state.as_str() {
1443            "applied" => ExactMutationOutcomeV1::Resolved,
1444            "failed" => ExactMutationOutcomeV1::Conflict,
1445            "expired_unknown" => ExactMutationOutcomeV1::KnownRetired,
1446            "prepared" => ExactMutationOutcomeV1::NotSubmitted,
1447            "pending" => ExactMutationOutcomeV1::Indeterminate,
1448            _ => return Err(ExactWorkflowError::CoreShape),
1449        };
1450        record_resolution_observation(&session, &mut recovery, resolution_disposition(outcome)).await?;
1451        if let Some(ExactTerminalResultV1::Index(result)) = &recovery.terminal {
1452            return Ok(execution(result.clone(), recovery));
1453        }
1454        let mut result = ReconcileIndexExactResultV1 {
1455            outcome,
1456            name: request.name,
1457            locator: Some(value.locator),
1458            resource: value.resource,
1459            state: value.state,
1460        };
1461        if outcome == ExactMutationOutcomeV1::Resolved {
1462            let Some(verify_options) = resolution_options(now, &options)? else {
1463                result.outcome = ExactMutationOutcomeV1::Indeterminate;
1464                return Ok(execution(result, recovery));
1465            };
1466            let existing = match self
1467                .get_index(
1468                    GetIndexRequest {
1469                        name: result.name.clone(),
1470                        expected_incarnation: result.resource.as_ref().map(|value| value.incarnation.clone()),
1471                    },
1472                    verify_options,
1473                )
1474                .await
1475            {
1476                Ok(value) => value.resource,
1477                Err(_) => {
1478                    result.outcome = ExactMutationOutcomeV1::Indeterminate;
1479                    return Ok(execution(result, recovery));
1480                }
1481            };
1482            if existing.current_mapping != request.mapping
1483                || request.resource_spec.as_ref() != Some(&existing.resource_spec)
1484                || existing.summary.retained_event_identity != request.retained_event_identity.unwrap_or(false)
1485            {
1486                result.outcome = ExactMutationOutcomeV1::Conflict;
1487                result.state = "definition_conflict".to_owned();
1488            } else if existing.summary.state != "active" {
1489                result.outcome = ExactMutationOutcomeV1::Indeterminate;
1490                result.state = existing.summary.state;
1491                return Ok(execution(result, recovery));
1492            } else {
1493                result.resource = Some(Resource {
1494                    kind: "index".to_owned(),
1495                    name: existing.summary.name,
1496                    incarnation: existing.summary.incarnation,
1497                });
1498                result.state = "active".to_owned();
1499            }
1500        }
1501        if matches!(
1502            outcome,
1503            ExactMutationOutcomeV1::Indeterminate | ExactMutationOutcomeV1::NotSubmitted
1504        ) {
1505            Ok(execution(result, recovery))
1506        } else {
1507            finish_index(&session, &mut recovery, result).await
1508        }
1509    }
1510
1511    fn exact_profile_scope(&self) -> (Identity, Identity) {
1512        let profile = self.connection_profile().as_wire();
1513        (
1514            profile.expected_cluster_id.clone(),
1515            profile.expected_collection_id.clone(),
1516        )
1517    }
1518}
1519
1520fn catalog_resolution_requires_exact_replay(state: &str, submission: Option<ExactSubmissionDispositionV1>) -> bool {
1521    state == "pending"
1522        || state == "prepared"
1523            && matches!(
1524                submission,
1525                Some(
1526                    ExactSubmissionDispositionV1::Submitted
1527                        | ExactSubmissionDispositionV1::Resolved
1528                        | ExactSubmissionDispositionV1::Indeterminate
1529                )
1530            )
1531}
1532
1533async fn observe_append_visibility(
1534    client: &Client,
1535    request: &AppendExactRequestV1,
1536    mut result: AppendExactResultV1,
1537    options: CallOptions,
1538) -> Result<AppendExactResultV1, ExactWorkflowError> {
1539    let mut visibility = Vec::with_capacity(result.items.len());
1540    for item in &result.items {
1541        let Some(locator) = item.locator.clone() else {
1542            return Err(ExactWorkflowError::CoreShape);
1543        };
1544        match client
1545            .observe_append_committed(
1546                ObserveAppendCommittedRequest {
1547                    index: request.index.clone(),
1548                    locator,
1549                },
1550                options.clone(),
1551            )
1552            .await
1553        {
1554            Ok(value) => visibility.push(value.outcome),
1555            Err(_) => {
1556                result.outcome = ExactMutationOutcomeV1::Indeterminate;
1557                result.visibility = visibility;
1558                return Ok(result);
1559            }
1560        }
1561    }
1562    if visibility
1563        .iter()
1564        .any(|value| value.state != RetainedAppendObservationState::Visible)
1565    {
1566        result.outcome = ExactMutationOutcomeV1::Indeterminate;
1567    }
1568    result.visibility = visibility;
1569    Ok(result)
1570}
1571
1572fn exact_intent(
1573    mutation_kind: ExactMutationKindV1,
1574    request_sha256: Digest,
1575    cluster_id: Identity,
1576    collection_id: Identity,
1577    index: Option<Resource>,
1578    caller_action_id: Digest,
1579    effect_deadline_unix_millis: u64,
1580) -> ExactMutationIntent {
1581    ExactMutationIntent {
1582        format_version: EXACT_MUTATION_FORMAT_VERSION,
1583        mutation_kind,
1584        request_sha256,
1585        cluster_id,
1586        collection_id,
1587        index,
1588        caller_action_id,
1589        effect_deadline_unix_millis: wire_u64(effect_deadline_unix_millis),
1590    }
1591}
1592
1593async fn open_session(
1594    intent: &ExactMutationIntent,
1595    options: &ExactWorkflowOptions,
1596) -> Result<(ExactSession, ExactWorkflowRecoveryV2), ExactWorkflowError> {
1597    intent.validate()?;
1598    if options.effect_deadline_unix_millis == 0 || options.store.is_some() && options.recovery.is_some() {
1599        return Err(ExactWorkflowError::InvalidRecovery);
1600    }
1601    let (session, recovery) = if let Some(store) = &options.store {
1602        let initial = recovery_from_intent(intent);
1603        let loaded = store.load().await.map_err(|error| store_failure(error, None))?;
1604        let recovery = match loaded {
1605            Some(value) => {
1606                validate_store_recovery(&value, intent)?;
1607                value
1608            }
1609            None => match store.compare_exchange(None, initial.clone()).await {
1610                Ok(()) => initial,
1611                Err(ExactWorkflowStoreError::Conflict {
1612                    observed: Some(observed),
1613                }) => {
1614                    let observed = *observed;
1615                    validate_store_recovery(&observed, intent)?;
1616                    observed
1617                }
1618                Err(ExactWorkflowStoreError::Conflict { observed: None }) => {
1619                    return Err(store_failure(ExactWorkflowStoreError::Malformed, None));
1620                }
1621                Err(error) => return Err(store_failure(error, None)),
1622            },
1623        };
1624        let conflict_deadline = options
1625            .observation_deadline_unix_millis
1626            .unwrap_or(options.effect_deadline_unix_millis);
1627        (
1628            ExactSession::Durable {
1629                store: Arc::clone(store),
1630                conflict_limit: options.store_conflict_limit,
1631                deadline_unix_millis: conflict_deadline,
1632                clock: Arc::clone(&options.clock),
1633                cancellation: options.cancellation.clone(),
1634            },
1635            recovery,
1636        )
1637    } else {
1638        let recovery = match &options.recovery {
1639            Some(value) => {
1640                validate_recovery_matches(value, intent)?;
1641                value.clone()
1642            }
1643            None => recovery_from_intent(intent),
1644        };
1645        (ExactSession::Ephemeral, recovery)
1646    };
1647    Ok((session, recovery))
1648}
1649
1650fn recovery_from_intent(intent: &ExactMutationIntent) -> ExactWorkflowRecoveryV2 {
1651    ExactWorkflowRecoveryV2 {
1652        format_version: intent.format_version,
1653        revision: 0,
1654        phase: ExactWorkflowPhaseV2::Initialized,
1655        mutation_kind: intent.mutation_kind,
1656        request_sha256: intent.request_sha256.clone(),
1657        cluster_id: intent.cluster_id.clone(),
1658        collection_id: intent.collection_id.clone(),
1659        index: intent.index.clone(),
1660        caller_action_id: intent.caller_action_id.clone(),
1661        effect_deadline_unix_millis: intent.effect_deadline_unix_millis.clone(),
1662        last_observed_unix_millis: None,
1663        preparation: None,
1664        submission_disposition: None,
1665        terminal: None,
1666    }
1667}
1668
1669fn validate_recovery_matches(
1670    recovery: &ExactWorkflowRecoveryV2,
1671    intent: &ExactMutationIntent,
1672) -> Result<(), ExactWorkflowError> {
1673    let expected = recovery_from_intent(intent);
1674    validate_recovery_shape(recovery)?;
1675    if recovery.format_version != expected.format_version
1676        || recovery.mutation_kind != expected.mutation_kind
1677        || recovery.request_sha256 != expected.request_sha256
1678        || recovery.cluster_id != expected.cluster_id
1679        || recovery.collection_id != expected.collection_id
1680        || recovery.index != expected.index
1681        || recovery.caller_action_id != expected.caller_action_id
1682    {
1683        return Err(ExactWorkflowError::InvalidRecovery);
1684    }
1685    Ok(())
1686}
1687
1688fn reduce_transition(
1689    state: &mut ExactWorkflowRecoveryV2,
1690    transition: &ExactMutationTransition,
1691) -> Result<(), ExactWorkflowError> {
1692    if state.revision >= MAX_SAFE_REVISION || state.phase == ExactWorkflowPhaseV2::Terminal {
1693        return Err(ExactWorkflowError::InvalidRecovery);
1694    }
1695    match transition {
1696        ExactMutationTransition::ClockObserved { unix_millis } => {
1697            let next = parse_u64(unix_millis).ok_or(ExactWorkflowError::InvalidRecovery)?;
1698            if state
1699                .last_observed_unix_millis
1700                .as_ref()
1701                .and_then(parse_u64)
1702                .is_some_and(|prior| next < prior)
1703            {
1704                return Err(ExactWorkflowError::InvalidRecovery);
1705            }
1706            state.last_observed_unix_millis = Some(unix_millis.clone());
1707        }
1708        ExactMutationTransition::Prepared { preparation }
1709            if state.preparation.is_none()
1710                && state.phase == ExactWorkflowPhaseV2::Initialized
1711                && preparation_kind(preparation) == state.mutation_kind =>
1712        {
1713            state.preparation = Some(preparation.clone());
1714            state.phase = ExactWorkflowPhaseV2::Prepared;
1715        }
1716        ExactMutationTransition::SubmissionStarted if state.preparation.is_some() && state.terminal.is_none() => {
1717            if state.phase < ExactWorkflowPhaseV2::SubmissionStarted {
1718                state.phase = ExactWorkflowPhaseV2::SubmissionStarted;
1719            } else if state.submission_disposition == Some(ExactSubmissionDispositionV1::NotSubmitted) {
1720                // INVARIANT: before a retry that follows proof of no effect,
1721                // clear that proof to the conservative may-have-submitted
1722                // state before the transport can observe the request.
1723                state.submission_disposition = Some(ExactSubmissionDispositionV1::Indeterminate);
1724            } else {
1725                return Err(ExactWorkflowError::InvalidRecovery);
1726            }
1727        }
1728        ExactMutationTransition::SubmissionObserved { disposition }
1729            if state.preparation.is_some()
1730                && state.terminal.is_none()
1731                && (state.phase >= ExactWorkflowPhaseV2::SubmissionStarted
1732                    || state.phase == ExactWorkflowPhaseV2::Prepared
1733                        && *disposition == ExactSubmissionDispositionV1::NotSubmitted)
1734                && valid_disposition_successor(state.submission_disposition, *disposition) =>
1735        {
1736            state.submission_disposition = Some(*disposition);
1737            state.phase = ExactWorkflowPhaseV2::SubmissionObserved;
1738        }
1739        ExactMutationTransition::Terminal { result }
1740            if state.terminal.is_none() && terminal_kind(result) == state.mutation_kind =>
1741        {
1742            state.terminal = Some(result.clone());
1743            state.phase = ExactWorkflowPhaseV2::Terminal;
1744        }
1745        _ => return Err(ExactWorkflowError::InvalidRecovery),
1746    }
1747    state.revision = state
1748        .revision
1749        .checked_add(1)
1750        .filter(|value| *value <= MAX_SAFE_REVISION)
1751        .ok_or(ExactWorkflowError::InvalidRecovery)?;
1752    validate_recovery_shape(state)?;
1753    Ok(())
1754}
1755
1756fn validate_recovery_shape(recovery: &ExactWorkflowRecoveryV2) -> Result<(), ExactWorkflowError> {
1757    let preparation_valid = recovery
1758        .preparation
1759        .as_ref()
1760        .is_none_or(|value| valid_preparation(value) && preparation_kind(value) == recovery.mutation_kind);
1761    let terminal_valid = recovery
1762        .terminal
1763        .as_ref()
1764        .is_none_or(|value| valid_terminal(value) && terminal_kind(value) == recovery.mutation_kind);
1765    let phase_valid = match recovery.phase {
1766        ExactWorkflowPhaseV2::Initialized => {
1767            recovery.preparation.is_none() && recovery.submission_disposition.is_none() && recovery.terminal.is_none()
1768        }
1769        ExactWorkflowPhaseV2::Prepared | ExactWorkflowPhaseV2::SubmissionStarted => {
1770            recovery.preparation.is_some() && recovery.submission_disposition.is_none() && recovery.terminal.is_none()
1771        }
1772        ExactWorkflowPhaseV2::SubmissionObserved => {
1773            recovery.preparation.is_some() && recovery.submission_disposition.is_some() && recovery.terminal.is_none()
1774        }
1775        ExactWorkflowPhaseV2::Terminal => recovery.terminal.is_some(),
1776    };
1777    if recovery.format_version != EXACT_MUTATION_FORMAT_VERSION
1778        || recovery.revision > MAX_SAFE_REVISION
1779        || recovery.request_sha256.validate().is_err()
1780        || recovery.cluster_id.validate().is_err()
1781        || recovery.collection_id.validate().is_err()
1782        || recovery.caller_action_id.validate().is_err()
1783        || recovery.index.as_ref().is_some_and(|value| !valid_index(value))
1784        || parse_u64(&recovery.effect_deadline_unix_millis).is_none_or(|value| value == 0)
1785        || recovery
1786            .last_observed_unix_millis
1787            .as_ref()
1788            .is_some_and(|value| parse_u64(value).is_none())
1789        || !preparation_valid
1790        || !terminal_valid
1791        || !phase_valid
1792        || recovery.revision == 0
1793            && (recovery.phase != ExactWorkflowPhaseV2::Initialized || recovery.last_observed_unix_millis.is_some())
1794    {
1795        return Err(ExactWorkflowError::InvalidRecovery);
1796    }
1797    Ok(())
1798}
1799
1800const fn valid_disposition_successor(
1801    current: Option<ExactSubmissionDispositionV1>,
1802    next: ExactSubmissionDispositionV1,
1803) -> bool {
1804    match current {
1805        None | Some(ExactSubmissionDispositionV1::NotSubmitted) => true,
1806        Some(ExactSubmissionDispositionV1::Submitted) => !matches!(next, ExactSubmissionDispositionV1::NotSubmitted),
1807        Some(ExactSubmissionDispositionV1::Indeterminate) => true,
1808        Some(ExactSubmissionDispositionV1::Resolved) => matches!(next, ExactSubmissionDispositionV1::Resolved),
1809        Some(ExactSubmissionDispositionV1::KnownRetired) => {
1810            matches!(next, ExactSubmissionDispositionV1::KnownRetired)
1811        }
1812    }
1813}
1814
1815fn validate_initial_recovery(recovery: &ExactWorkflowRecoveryV2) -> Result<(), ExactWorkflowStoreError> {
1816    validate_recovery_shape(recovery).map_err(|_| ExactWorkflowStoreError::Malformed)?;
1817    if recovery.revision != 0 || recovery.phase != ExactWorkflowPhaseV2::Initialized {
1818        return Err(ExactWorkflowStoreError::Malformed);
1819    }
1820    Ok(())
1821}
1822
1823fn intent_from_recovery(recovery: &ExactWorkflowRecoveryV2) -> ExactMutationIntent {
1824    ExactMutationIntent {
1825        format_version: recovery.format_version,
1826        mutation_kind: recovery.mutation_kind,
1827        request_sha256: recovery.request_sha256.clone(),
1828        cluster_id: recovery.cluster_id.clone(),
1829        collection_id: recovery.collection_id.clone(),
1830        index: recovery.index.clone(),
1831        caller_action_id: recovery.caller_action_id.clone(),
1832        effect_deadline_unix_millis: recovery.effect_deadline_unix_millis.clone(),
1833    }
1834}
1835
1836fn transition_between(
1837    current: &ExactWorkflowRecoveryV2,
1838    next: &ExactWorkflowRecoveryV2,
1839) -> Result<ExactMutationTransition, ExactWorkflowStoreError> {
1840    validate_recovery_shape(current).map_err(|_| ExactWorkflowStoreError::Malformed)?;
1841    validate_recovery_shape(next).map_err(|_| ExactWorkflowStoreError::Malformed)?;
1842    if next.revision
1843        != current
1844            .revision
1845            .checked_add(1)
1846            .ok_or(ExactWorkflowStoreError::Malformed)?
1847        || !same_recovery_intent(current, next)
1848    {
1849        return Err(ExactWorkflowStoreError::Malformed);
1850    }
1851    let retry_started = current.submission_disposition == Some(ExactSubmissionDispositionV1::NotSubmitted)
1852        && next.submission_disposition == Some(ExactSubmissionDispositionV1::Indeterminate)
1853        && current.phase == next.phase;
1854    let candidates = [
1855        next.last_observed_unix_millis
1856            .as_ref()
1857            .filter(|value| current.last_observed_unix_millis.as_ref() != Some(*value))
1858            .map(|value| ExactMutationTransition::ClockObserved {
1859                unix_millis: value.clone(),
1860            }),
1861        next.preparation
1862            .as_ref()
1863            .filter(|value| current.preparation.as_ref() != Some(*value))
1864            .map(|value| ExactMutationTransition::Prepared {
1865                preparation: value.clone(),
1866            }),
1867        (retry_started
1868            || current.phase < ExactWorkflowPhaseV2::SubmissionStarted
1869                && next.phase >= ExactWorkflowPhaseV2::SubmissionStarted)
1870            .then_some(ExactMutationTransition::SubmissionStarted),
1871        next.submission_disposition
1872            .filter(|_| !retry_started)
1873            .map(|disposition| ExactMutationTransition::SubmissionObserved { disposition }),
1874        next.terminal
1875            .as_ref()
1876            .filter(|value| current.terminal.as_ref() != Some(*value))
1877            .map(|value| ExactMutationTransition::Terminal { result: value.clone() }),
1878    ];
1879    let mut matched = None;
1880    for candidate in candidates.into_iter().flatten() {
1881        let mut reduced = current.clone();
1882        if reduce_transition(&mut reduced, &candidate).is_ok() && reduced == *next {
1883            if matched.is_some() {
1884                return Err(ExactWorkflowStoreError::Malformed);
1885            }
1886            matched = Some(candidate);
1887        }
1888    }
1889    matched.ok_or(ExactWorkflowStoreError::Malformed)
1890}
1891
1892fn validate_store_recovery(
1893    recovery: &ExactWorkflowRecoveryV2,
1894    intent: &ExactMutationIntent,
1895) -> Result<(), ExactWorkflowError> {
1896    if recovery.format_version != EXACT_MUTATION_FORMAT_VERSION {
1897        return Err(store_failure(
1898            ExactWorkflowStoreError::UnsupportedFormat {
1899                observed: recovery.format_version,
1900            },
1901            Some(recovery.clone()),
1902        ));
1903    }
1904    validate_recovery_shape(recovery)
1905        .map_err(|_| store_failure(ExactWorkflowStoreError::Malformed, Some(recovery.clone())))?;
1906    if !recovery_matches_intent(recovery, intent) {
1907        return Err(store_failure(
1908            ExactWorkflowStoreError::IntentMismatch,
1909            Some(recovery.clone()),
1910        ));
1911    }
1912    Ok(())
1913}
1914
1915fn validate_recovery_descendant(
1916    current: &ExactWorkflowRecoveryV2,
1917    observed: &ExactWorkflowRecoveryV2,
1918) -> Result<(), ExactWorkflowError> {
1919    if observed.format_version != EXACT_MUTATION_FORMAT_VERSION {
1920        return Err(store_failure(
1921            ExactWorkflowStoreError::UnsupportedFormat {
1922                observed: observed.format_version,
1923            },
1924            Some(current.clone()),
1925        ));
1926    }
1927    validate_recovery_shape(observed)
1928        .map_err(|_| store_failure(ExactWorkflowStoreError::Malformed, Some(current.clone())))?;
1929    let clock_monotonic = current.last_observed_unix_millis.as_ref().is_none_or(|prior| {
1930        observed
1931            .last_observed_unix_millis
1932            .as_ref()
1933            .and_then(parse_u64)
1934            .zip(parse_u64(prior))
1935            .is_some_and(|(next, prior)| next >= prior)
1936    });
1937    let disposition_monotonic = current.submission_disposition.is_none_or(|prior| {
1938        observed
1939            .submission_disposition
1940            .is_some_and(|next| valid_disposition_successor(Some(prior), next))
1941    });
1942    if !same_recovery_intent(current, observed) {
1943        return Err(store_failure(
1944            ExactWorkflowStoreError::IntentMismatch,
1945            Some(current.clone()),
1946        ));
1947    }
1948    if observed.revision <= current.revision
1949        || observed.phase < current.phase
1950        || !clock_monotonic
1951        || current
1952            .preparation
1953            .as_ref()
1954            .is_some_and(|value| observed.preparation.as_ref() != Some(value))
1955        || !disposition_monotonic
1956        || current
1957            .terminal
1958            .as_ref()
1959            .is_some_and(|value| observed.terminal.as_ref() != Some(value))
1960    {
1961        return Err(store_failure(ExactWorkflowStoreError::Malformed, Some(current.clone())));
1962    }
1963    Ok(())
1964}
1965
1966fn transition_is_reflected(state: &ExactWorkflowRecoveryV2, transition: &ExactMutationTransition) -> bool {
1967    if state.terminal.is_some() {
1968        // INVARIANT: the first CAS-committed terminal result is authoritative.
1969        // A losing replica must adopt it instead of attempting to overwrite it
1970        // or returning a result derived from stale local observations.
1971        return true;
1972    }
1973    match transition {
1974        ExactMutationTransition::ClockObserved { unix_millis } => parse_u64(unix_millis).is_some_and(|requested| {
1975            state
1976                .last_observed_unix_millis
1977                .as_ref()
1978                .and_then(parse_u64)
1979                .is_some_and(|observed| observed >= requested)
1980        }),
1981        ExactMutationTransition::Prepared { preparation } => state.preparation.as_ref() == Some(preparation),
1982        ExactMutationTransition::SubmissionStarted => {
1983            state.preparation.is_some()
1984                && (state.phase == ExactWorkflowPhaseV2::SubmissionStarted
1985                    || state.phase >= ExactWorkflowPhaseV2::SubmissionObserved
1986                        && state.submission_disposition != Some(ExactSubmissionDispositionV1::NotSubmitted))
1987        }
1988        ExactMutationTransition::SubmissionObserved { disposition } => state
1989            .submission_disposition
1990            .is_some_and(|observed| valid_disposition_successor(Some(*disposition), observed)),
1991        ExactMutationTransition::Terminal { .. } => false,
1992    }
1993}
1994
1995fn same_recovery_intent(left: &ExactWorkflowRecoveryV2, right: &ExactWorkflowRecoveryV2) -> bool {
1996    left.format_version == right.format_version
1997        && left.mutation_kind == right.mutation_kind
1998        && left.request_sha256 == right.request_sha256
1999        && left.cluster_id == right.cluster_id
2000        && left.collection_id == right.collection_id
2001        && left.index == right.index
2002        && left.caller_action_id == right.caller_action_id
2003        && left.effect_deadline_unix_millis == right.effect_deadline_unix_millis
2004}
2005
2006fn recovery_matches_intent(recovery: &ExactWorkflowRecoveryV2, intent: &ExactMutationIntent) -> bool {
2007    recovery.format_version == intent.format_version
2008        && recovery.mutation_kind == intent.mutation_kind
2009        && recovery.request_sha256 == intent.request_sha256
2010        && recovery.cluster_id == intent.cluster_id
2011        && recovery.collection_id == intent.collection_id
2012        && recovery.index == intent.index
2013        && recovery.caller_action_id == intent.caller_action_id
2014}
2015
2016fn store_failure(source: ExactWorkflowStoreError, recovery: Option<ExactWorkflowRecoveryV2>) -> ExactWorkflowError {
2017    ExactWorkflowError::Store {
2018        source,
2019        last_durable_recovery: recovery.map(Box::new),
2020    }
2021}
2022
2023async fn record_resolution_observation(
2024    session: &ExactSession,
2025    recovery: &mut ExactWorkflowRecoveryV2,
2026    disposition: ExactSubmissionDispositionV1,
2027) -> Result<(), ExactWorkflowError> {
2028    // INVARIANT: a reflected disposition is as authoritative as one committed
2029    // by this caller. Only SubmissionStarted ownership controls whether this
2030    // caller may perform an effect; resolution and visibility remain reads.
2031    session
2032        .record(recovery, ExactMutationTransition::SubmissionObserved { disposition })
2033        .await
2034        .map(|_| ())
2035}
2036
2037async fn observe_clock(
2038    session: &ExactSession,
2039    recovery: &mut ExactWorkflowRecoveryV2,
2040    options: &ExactWorkflowOptions,
2041) -> Result<u64, ExactWorkflowError> {
2042    let now = options.clock.unix_millis()?;
2043    // INVARIANT: once submission may have happened, later wall-clock samples
2044    // cannot fence another effect. Only an authoritative not-submitted result
2045    // reopens submission, so visibility polling must not consume the bounded
2046    // durable journal with liveness-only clock observations.
2047    if recovery.phase >= ExactWorkflowPhaseV2::SubmissionStarted
2048        && recovery.submission_disposition != Some(ExactSubmissionDispositionV1::NotSubmitted)
2049    {
2050        return Ok(now);
2051    }
2052    if recovery
2053        .last_observed_unix_millis
2054        .as_ref()
2055        .and_then(parse_u64)
2056        .is_some_and(|prior| now <= prior)
2057    {
2058        return Ok(now);
2059    }
2060    session
2061        .record(
2062            recovery,
2063            ExactMutationTransition::ClockObserved {
2064                unix_millis: wire_u64(now),
2065            },
2066        )
2067        .await?;
2068    Ok(now)
2069}
2070
2071fn clock_regressed(recovery: &ExactWorkflowRecoveryV2, now: u64) -> bool {
2072    recovery
2073        .last_observed_unix_millis
2074        .as_ref()
2075        .and_then(parse_u64)
2076        .is_some_and(|observed| now < observed)
2077}
2078
2079fn recovery_effect_deadline(recovery: &ExactWorkflowRecoveryV2) -> Result<u64, ExactWorkflowError> {
2080    parse_u64(&recovery.effect_deadline_unix_millis)
2081        .filter(|value| *value > 0)
2082        .ok_or(ExactWorkflowError::InvalidRecovery)
2083}
2084
2085fn effect_options(now: u64, deadline: u64, options: &ExactWorkflowOptions) -> Result<CallOptions, ExactWorkflowError> {
2086    bounded_options(now, deadline, options)
2087}
2088
2089fn resolution_options(now: u64, options: &ExactWorkflowOptions) -> Result<Option<CallOptions>, ExactWorkflowError> {
2090    let deadline = if now < options.effect_deadline_unix_millis {
2091        options.effect_deadline_unix_millis
2092    } else {
2093        match options.observation_deadline_unix_millis {
2094            Some(value) if value > now => value,
2095            _ => return Ok(None),
2096        }
2097    };
2098    bounded_options(now, deadline, options).map(Some)
2099}
2100
2101fn bounded_options(now: u64, deadline: u64, options: &ExactWorkflowOptions) -> Result<CallOptions, ExactWorkflowError> {
2102    if deadline <= now {
2103        return Err(ExactWorkflowError::InvalidInput);
2104    }
2105    let bounded = deadline.min(now.saturating_add(MAX_EFFECT_CALL_MILLIS));
2106    CallOptions::from_absolute_deadline_unix_millis(bounded)
2107        .map(|value| value.with_cancellation(options.cancellation.clone()))
2108        .map_err(ExactWorkflowError::Client)
2109}
2110
2111fn validate_append_request(request: &AppendExactRequestV1) -> Result<(), ExactWorkflowError> {
2112    if request.batch_id.validate().is_err()
2113        || !valid_index(&request.index)
2114        || request.items.is_empty()
2115        || request.items.len() > 1_000
2116        || request.items.iter().any(|item| {
2117            item.item_action_id.validate().is_err() || item.document_id.is_empty() || item.document_id.len() > 512
2118        })
2119    {
2120        return Err(ExactWorkflowError::InvalidInput);
2121    }
2122    let mut identities = std::collections::BTreeSet::new();
2123    if request
2124        .items
2125        .iter()
2126        .any(|item| !identities.insert(item.item_action_id.value.as_str()))
2127    {
2128        return Err(ExactWorkflowError::InvalidInput);
2129    }
2130    Ok(())
2131}
2132
2133fn validate_append_outcome(
2134    request: &AppendExactRequestV1,
2135    outcome: &RetainedAppendBulkOutcome,
2136    prepared: bool,
2137) -> Result<(), ExactWorkflowError> {
2138    if outcome.batch_id != request.batch_id
2139        || outcome.index != request.index
2140        || outcome.items.len() != request.items.len()
2141    {
2142        return Err(ExactWorkflowError::CoreShape);
2143    }
2144    for (expected, observed) in request.items.iter().zip(&outcome.items) {
2145        if observed.item_action_id != expected.item_action_id
2146            || observed
2147                .locator
2148                .as_ref()
2149                .is_some_and(|value| value.authority.is_empty() || value.value.is_empty())
2150            || observed
2151                .semantic_digest
2152                .as_ref()
2153                .is_some_and(|value| value.validate().is_err())
2154            || prepared
2155                && (observed.state != RetainedAppendState::Prepared
2156                    || observed.retry_safety != RetainedAppendRetrySafety::Safe
2157                    || observed.locator.is_none()
2158                    || observed.semantic_digest.is_none()
2159                    || observed.sequence.is_some()
2160                    || observed.primary_term.is_some()
2161                    || observed.error.is_some())
2162        {
2163            return Err(ExactWorkflowError::CoreShape);
2164        }
2165    }
2166    Ok(())
2167}
2168
2169fn redact_append_outcome(mut outcome: RetainedAppendBulkOutcome) -> RetainedAppendBulkOutcome {
2170    for item in &mut outcome.items {
2171        item.error = None;
2172    }
2173    outcome
2174}
2175
2176fn append_preparation(recovery: &ExactWorkflowRecoveryV2) -> Result<&RetainedAppendBulkOutcome, ExactWorkflowError> {
2177    match &recovery.preparation {
2178        Some(ExactPreparationV1::Append(value)) => Ok(value),
2179        _ => Err(ExactWorkflowError::InvalidRecovery),
2180    }
2181}
2182
2183fn document_preparation(recovery: &ExactWorkflowRecoveryV2) -> Result<(Locator, Digest), ExactWorkflowError> {
2184    match &recovery.preparation {
2185        Some(ExactPreparationV1::Document {
2186            locator,
2187            semantic_digest,
2188        }) => Ok((locator.clone(), semantic_digest.clone())),
2189        _ => Err(ExactWorkflowError::InvalidRecovery),
2190    }
2191}
2192
2193fn index_preparation(
2194    recovery: &ExactWorkflowRecoveryV2,
2195) -> Result<(Locator, U64, Digest, IndexResourceSpec), ExactWorkflowError> {
2196    match &recovery.preparation {
2197        Some(ExactPreparationV1::Index {
2198            locator,
2199            expires_at,
2200            semantic_digest,
2201            resource_spec,
2202        }) => Ok((
2203            locator.clone(),
2204            expires_at.clone(),
2205            semantic_digest.clone(),
2206            resource_spec.as_ref().clone(),
2207        )),
2208        _ => Err(ExactWorkflowError::InvalidRecovery),
2209    }
2210}
2211
2212fn may_start_catalog_submission(
2213    recovery: &ExactWorkflowRecoveryV2,
2214    now_unix_millis: u64,
2215    effect_deadline_unix_millis: u64,
2216) -> bool {
2217    // The preparation's authority-time expiry is deliberately absent here.
2218    // It cannot be compared with the caller's Unix clock.
2219    (recovery.phase == ExactWorkflowPhaseV2::Prepared
2220        || recovery.submission_disposition == Some(ExactSubmissionDispositionV1::NotSubmitted))
2221        && now_unix_millis < effect_deadline_unix_millis
2222}
2223
2224fn append_terminal_result(
2225    request: &AppendExactRequestV1,
2226    outcome: &RetainedAppendBulkOutcome,
2227    recovered: bool,
2228) -> Option<AppendExactResultV1> {
2229    let state = if outcome
2230        .items
2231        .iter()
2232        .any(|item| item.state == RetainedAppendState::SubmittedIndeterminate)
2233    {
2234        return None;
2235    } else if outcome
2236        .items
2237        .iter()
2238        .any(|item| item.state == RetainedAppendState::Conflict)
2239    {
2240        ExactMutationOutcomeV1::Conflict
2241    } else if outcome
2242        .items
2243        .iter()
2244        .any(|item| item.state == RetainedAppendState::Retired)
2245    {
2246        ExactMutationOutcomeV1::KnownRetired
2247    } else if outcome.items.iter().any(|item| {
2248        matches!(
2249            item.state,
2250            RetainedAppendState::NotSubmittedRetryable | RetainedAppendState::NotSubmittedTerminal
2251        )
2252    }) {
2253        ExactMutationOutcomeV1::NotSubmitted
2254    } else if outcome.items.iter().all(|item| {
2255        matches!(
2256            item.state,
2257            RetainedAppendState::Committed | RetainedAppendState::Replayed
2258        )
2259    }) {
2260        if recovered {
2261            ExactMutationOutcomeV1::Resolved
2262        } else {
2263            ExactMutationOutcomeV1::Applied
2264        }
2265    } else {
2266        return None;
2267    };
2268    Some(append_result(request, outcome, state))
2269}
2270
2271fn append_result(
2272    request: &AppendExactRequestV1,
2273    outcome: &RetainedAppendBulkOutcome,
2274    state: ExactMutationOutcomeV1,
2275) -> AppendExactResultV1 {
2276    AppendExactResultV1 {
2277        outcome: state,
2278        batch_id: request.batch_id.clone(),
2279        index: request.index.clone(),
2280        items: outcome.items.iter().map(redact_append_item).collect(),
2281        visibility: Vec::new(),
2282    }
2283}
2284
2285fn append_status(
2286    request: &AppendExactRequestV1,
2287    recovery: &ExactWorkflowRecoveryV2,
2288    state: ExactMutationOutcomeV1,
2289) -> AppendExactResultV1 {
2290    let items = append_preparation(recovery)
2291        .map(|value| value.items.iter().map(redact_append_item).collect())
2292        .unwrap_or_default();
2293    AppendExactResultV1 {
2294        outcome: state,
2295        batch_id: request.batch_id.clone(),
2296        index: request.index.clone(),
2297        items,
2298        visibility: Vec::new(),
2299    }
2300}
2301
2302fn redact_append_item(item: &RetainedAppendItemOutcome) -> AppendExactItemResultV1 {
2303    AppendExactItemResultV1 {
2304        item_action_id: item.item_action_id.clone(),
2305        locator: item.locator.clone(),
2306        semantic_digest: item.semantic_digest.clone(),
2307        state: item.state,
2308        retry_safety: item.retry_safety,
2309        sequence: item.sequence.clone(),
2310        primary_term: item.primary_term.clone(),
2311    }
2312}
2313
2314fn document_status(
2315    request: &PrepareDocumentMutationRequest,
2316    recovery: &ExactWorkflowRecoveryV2,
2317    outcome: ExactMutationOutcomeV1,
2318) -> DocumentMutationExactResultV1 {
2319    let locator = document_preparation(recovery).map(|value| value.0).ok();
2320    DocumentMutationExactResultV1 {
2321        outcome,
2322        index: request.index.clone(),
2323        locator,
2324        state: outcome_name(outcome).to_owned(),
2325        submitted: recovery
2326            .submission_disposition
2327            .is_some_and(|value| !matches!(value, ExactSubmissionDispositionV1::NotSubmitted)),
2328        sequence: None,
2329        mapping_generation: None,
2330        mapping_adopted: false,
2331    }
2332}
2333
2334fn index_status(
2335    request: &PrepareCreateIndexRequest,
2336    recovery: &ExactWorkflowRecoveryV2,
2337    outcome: ExactMutationOutcomeV1,
2338) -> ReconcileIndexExactResultV1 {
2339    let locator = match &recovery.preparation {
2340        Some(ExactPreparationV1::Index { locator, .. }) => Some(locator.clone()),
2341        _ => None,
2342    };
2343    ReconcileIndexExactResultV1 {
2344        outcome,
2345        name: request.name.clone(),
2346        locator,
2347        resource: None,
2348        state: outcome_name(outcome).to_owned(),
2349    }
2350}
2351
2352const fn outcome_name(outcome: ExactMutationOutcomeV1) -> &'static str {
2353    match outcome {
2354        ExactMutationOutcomeV1::AlreadySatisfied => "already_satisfied",
2355        ExactMutationOutcomeV1::Applied => "applied",
2356        ExactMutationOutcomeV1::Resolved => "resolved",
2357        ExactMutationOutcomeV1::Conflict => "conflict",
2358        ExactMutationOutcomeV1::NotSubmitted => "not_submitted",
2359        ExactMutationOutcomeV1::Indeterminate => "indeterminate",
2360        ExactMutationOutcomeV1::KnownRetired => "known_retired",
2361        ExactMutationOutcomeV1::EffectDeadlineExpired => "effect_deadline_expired",
2362        ExactMutationOutcomeV1::ClockRegression => "clock_regression",
2363    }
2364}
2365
2366async fn finish_append(
2367    session: &ExactSession,
2368    recovery: &mut ExactWorkflowRecoveryV2,
2369    result: AppendExactResultV1,
2370) -> Result<ExactExecutionV2<AppendExactResultV1>, ExactWorkflowError> {
2371    session
2372        .record(
2373            recovery,
2374            ExactMutationTransition::Terminal {
2375                result: ExactTerminalResultV1::Append(result.clone()),
2376            },
2377        )
2378        .await?;
2379    let durable = match &recovery.terminal {
2380        Some(ExactTerminalResultV1::Append(durable)) => durable.clone(),
2381        _ => return Err(ExactWorkflowError::InvalidRecovery),
2382    };
2383    Ok(execution(durable, recovery.clone()))
2384}
2385
2386async fn finish_document(
2387    session: &ExactSession,
2388    recovery: &mut ExactWorkflowRecoveryV2,
2389    result: DocumentMutationExactResultV1,
2390) -> Result<ExactExecutionV2<DocumentMutationExactResultV1>, ExactWorkflowError> {
2391    session
2392        .record(
2393            recovery,
2394            ExactMutationTransition::Terminal {
2395                result: ExactTerminalResultV1::Document(result.clone()),
2396            },
2397        )
2398        .await?;
2399    let durable = match &recovery.terminal {
2400        Some(ExactTerminalResultV1::Document(durable)) => durable.clone(),
2401        _ => return Err(ExactWorkflowError::InvalidRecovery),
2402    };
2403    Ok(execution(durable, recovery.clone()))
2404}
2405
2406async fn finish_index(
2407    session: &ExactSession,
2408    recovery: &mut ExactWorkflowRecoveryV2,
2409    result: ReconcileIndexExactResultV1,
2410) -> Result<ExactExecutionV2<ReconcileIndexExactResultV1>, ExactWorkflowError> {
2411    session
2412        .record(
2413            recovery,
2414            ExactMutationTransition::Terminal {
2415                result: ExactTerminalResultV1::Index(result.clone()),
2416            },
2417        )
2418        .await?;
2419    let durable = match &recovery.terminal {
2420        Some(ExactTerminalResultV1::Index(durable)) => durable.clone(),
2421        _ => return Err(ExactWorkflowError::InvalidRecovery),
2422    };
2423    Ok(execution(durable, recovery.clone()))
2424}
2425
2426fn execution<T>(result: T, recovery: ExactWorkflowRecoveryV2) -> ExactExecutionV2<T> {
2427    ExactExecutionV2 { result, recovery }
2428}
2429
2430fn valid_preparation(value: &ExactPreparationV1) -> bool {
2431    match value {
2432        ExactPreparationV1::Append(outcome) => {
2433            !outcome.items.is_empty()
2434                && outcome.items.len() <= 1_000
2435                && outcome.items.iter().all(|item| {
2436                    item.item_action_id.validate().is_ok()
2437                        && item
2438                            .locator
2439                            .as_ref()
2440                            .is_some_and(|locator| !locator.authority.is_empty() && !locator.value.is_empty())
2441                        && item
2442                            .semantic_digest
2443                            .as_ref()
2444                            .is_some_and(|digest| digest.validate().is_ok())
2445                        && item.state == RetainedAppendState::Prepared
2446                        && item.retry_safety == RetainedAppendRetrySafety::Safe
2447                        && item.error.is_none()
2448                })
2449        }
2450        ExactPreparationV1::Document {
2451            locator,
2452            semantic_digest,
2453        } => !locator.authority.is_empty() && !locator.value.is_empty() && semantic_digest.validate().is_ok(),
2454        ExactPreparationV1::Index {
2455            locator,
2456            expires_at,
2457            semantic_digest,
2458            ..
2459        } => {
2460            !locator.authority.is_empty()
2461                && !locator.value.is_empty()
2462                && parse_u64(expires_at).is_some_and(|value| value > 0)
2463                && semantic_digest.validate().is_ok()
2464        }
2465    }
2466}
2467
2468fn valid_terminal(value: &ExactTerminalResultV1) -> bool {
2469    match value {
2470        ExactTerminalResultV1::Append(result) => {
2471            result.batch_id.validate().is_ok()
2472                && valid_index(&result.index)
2473                && terminal_outcome(result.outcome)
2474                && result.items.iter().all(|item| item.item_action_id.validate().is_ok())
2475        }
2476        ExactTerminalResultV1::Document(result) => {
2477            valid_index(&result.index)
2478                && result
2479                    .locator
2480                    .as_ref()
2481                    .is_none_or(|locator| !locator.authority.is_empty() && !locator.value.is_empty())
2482                && terminal_outcome(result.outcome)
2483        }
2484        ExactTerminalResultV1::Index(result) => !result.name.is_empty() && terminal_outcome(result.outcome),
2485    }
2486}
2487
2488const fn terminal_outcome(value: ExactMutationOutcomeV1) -> bool {
2489    matches!(
2490        value,
2491        ExactMutationOutcomeV1::AlreadySatisfied
2492            | ExactMutationOutcomeV1::Applied
2493            | ExactMutationOutcomeV1::Resolved
2494            | ExactMutationOutcomeV1::Conflict
2495            | ExactMutationOutcomeV1::KnownRetired
2496            | ExactMutationOutcomeV1::EffectDeadlineExpired
2497    )
2498}
2499
2500const fn preparation_kind(value: &ExactPreparationV1) -> ExactMutationKindV1 {
2501    match value {
2502        ExactPreparationV1::Append(_) => ExactMutationKindV1::Append,
2503        ExactPreparationV1::Document { .. } => ExactMutationKindV1::Document,
2504        ExactPreparationV1::Index { .. } => ExactMutationKindV1::Index,
2505    }
2506}
2507
2508const fn terminal_kind(value: &ExactTerminalResultV1) -> ExactMutationKindV1 {
2509    match value {
2510        ExactTerminalResultV1::Append(_) => ExactMutationKindV1::Append,
2511        ExactTerminalResultV1::Document(_) => ExactMutationKindV1::Document,
2512        ExactTerminalResultV1::Index(_) => ExactMutationKindV1::Index,
2513    }
2514}
2515
2516fn valid_index(value: &Resource) -> bool {
2517    value.kind == "index" && !value.name.is_empty() && value.name.len() <= 255 && value.incarnation.validate().is_ok()
2518}
2519
2520fn canonical_sha256<T: Serialize>(value: &T) -> Result<Digest, ExactWorkflowError> {
2521    let bytes = serde_json::to_vec(value).map_err(|_| ExactWorkflowError::RequestCodec)?;
2522    let observed = digest::digest(&digest::SHA256, &bytes);
2523    Ok(Digest {
2524        value: lower_hex(observed.as_ref()),
2525    })
2526}
2527
2528fn parse_u64(value: &U64) -> Option<u64> {
2529    if value.value.is_empty() || value.value.len() > 20 || value.value.starts_with('0') && value.value != "0" {
2530        return None;
2531    }
2532    value.value.parse().ok()
2533}
2534
2535fn wire_u64(value: u64) -> U64 {
2536    U64 {
2537        value: value.to_string(),
2538    }
2539}
2540
2541fn lower_hex(bytes: &[u8]) -> String {
2542    const HEX: &[u8; 16] = b"0123456789abcdef";
2543    let mut output = String::with_capacity(bytes.len().saturating_mul(2));
2544    for byte in bytes {
2545        output.push(char::from(HEX[usize::from(byte >> 4)]));
2546        output.push(char::from(HEX[usize::from(byte & 0x0f)]));
2547    }
2548    output
2549}
2550
2551#[cfg(test)]
2552mod tests {
2553    use std::os::unix::fs::PermissionsExt as _;
2554    use std::sync::Mutex;
2555
2556    use super::*;
2557
2558    #[derive(Default)]
2559    struct PostgresShapedRecoveryStore {
2560        row: Mutex<Option<ExactWorkflowRecoveryV2>>,
2561    }
2562
2563    impl ExactWorkflowRecoveryStore for PostgresShapedRecoveryStore {
2564        fn load(
2565            &self,
2566        ) -> ExactWorkflowStoreFuture<'_, Result<Option<ExactWorkflowRecoveryV2>, ExactWorkflowStoreError>> {
2567            Box::pin(async move {
2568                self.row
2569                    .lock()
2570                    .map(|row| row.clone())
2571                    .map_err(|_| ExactWorkflowStoreError::Internal)
2572            })
2573        }
2574
2575        fn compare_exchange(
2576            &self,
2577            expected_revision: Option<u64>,
2578            next: ExactWorkflowRecoveryV2,
2579        ) -> ExactWorkflowStoreFuture<'_, Result<(), ExactWorkflowStoreError>> {
2580            Box::pin(async move {
2581                let mut row = self.row.lock().map_err(|_| ExactWorkflowStoreError::Internal)?;
2582                let observed_revision = row.as_ref().map(|value| value.revision);
2583                if observed_revision != expected_revision {
2584                    return Err(ExactWorkflowStoreError::Conflict {
2585                        observed: row.clone().map(Box::new),
2586                    });
2587                }
2588                *row = Some(next);
2589                Ok(())
2590            })
2591        }
2592    }
2593
2594    struct AdvancingConflictStore {
2595        row: Mutex<ExactWorkflowRecoveryV2>,
2596    }
2597
2598    struct UnavailableRecoveryStore;
2599
2600    struct AdvancingClock {
2601        next: Mutex<u64>,
2602    }
2603
2604    impl ExactWorkflowClock for AdvancingClock {
2605        fn unix_millis(&self) -> Result<u64, ExactWorkflowError> {
2606            let mut next = self.next.lock().map_err(|_| ExactWorkflowError::ClockUnavailable)?;
2607            let observed = *next;
2608            *next = next.saturating_add(1);
2609            Ok(observed)
2610        }
2611    }
2612
2613    impl ExactWorkflowRecoveryStore for UnavailableRecoveryStore {
2614        fn load(
2615            &self,
2616        ) -> ExactWorkflowStoreFuture<'_, Result<Option<ExactWorkflowRecoveryV2>, ExactWorkflowStoreError>> {
2617            Box::pin(async { Err(ExactWorkflowStoreError::Unavailable) })
2618        }
2619
2620        fn compare_exchange(
2621            &self,
2622            _expected_revision: Option<u64>,
2623            _next: ExactWorkflowRecoveryV2,
2624        ) -> ExactWorkflowStoreFuture<'_, Result<(), ExactWorkflowStoreError>> {
2625            Box::pin(async { Err(ExactWorkflowStoreError::Unavailable) })
2626        }
2627    }
2628
2629    impl ExactWorkflowRecoveryStore for AdvancingConflictStore {
2630        fn load(
2631            &self,
2632        ) -> ExactWorkflowStoreFuture<'_, Result<Option<ExactWorkflowRecoveryV2>, ExactWorkflowStoreError>> {
2633            Box::pin(async move {
2634                self.row
2635                    .lock()
2636                    .map(|row| Some(row.clone()))
2637                    .map_err(|_| ExactWorkflowStoreError::Internal)
2638            })
2639        }
2640
2641        fn compare_exchange(
2642            &self,
2643            _expected_revision: Option<u64>,
2644            _next: ExactWorkflowRecoveryV2,
2645        ) -> ExactWorkflowStoreFuture<'_, Result<(), ExactWorkflowStoreError>> {
2646            Box::pin(async move {
2647                let mut row = self.row.lock().map_err(|_| ExactWorkflowStoreError::Internal)?;
2648                let next_clock = row
2649                    .last_observed_unix_millis
2650                    .as_ref()
2651                    .and_then(parse_u64)
2652                    .unwrap_or(0)
2653                    .saturating_add(1);
2654                reduce_transition(
2655                    &mut row,
2656                    &ExactMutationTransition::ClockObserved {
2657                        unix_millis: wire_u64(next_clock),
2658                    },
2659                )
2660                .map_err(|_| ExactWorkflowStoreError::Internal)?;
2661                Err(ExactWorkflowStoreError::Conflict {
2662                    observed: Some(Box::new(row.clone())),
2663                })
2664            })
2665        }
2666    }
2667
2668    fn identity(character: char) -> Identity {
2669        Identity {
2670            value: character.to_string().repeat(32),
2671        }
2672    }
2673
2674    fn digest(character: char) -> Digest {
2675        Digest {
2676            value: character.to_string().repeat(64),
2677        }
2678    }
2679
2680    fn resource() -> Resource {
2681        Resource {
2682            kind: "index".to_owned(),
2683            name: "events".to_owned(),
2684            incarnation: identity('a'),
2685        }
2686    }
2687
2688    fn append_preparation_value() -> ExactPreparationV1 {
2689        ExactPreparationV1::Append(RetainedAppendBulkOutcome {
2690            batch_id: digest('e'),
2691            index: resource(),
2692            items: vec![RetainedAppendItemOutcome {
2693                item_action_id: digest('f'),
2694                locator: Some(Locator {
2695                    authority: "retained_append".to_owned(),
2696                    value: "01".to_owned(),
2697                }),
2698                semantic_digest: Some(digest('1')),
2699                state: RetainedAppendState::Prepared,
2700                retry_safety: RetainedAppendRetrySafety::Safe,
2701                sequence: None,
2702                primary_term: None,
2703                error: None,
2704            }],
2705            complete: true,
2706            achievement: None,
2707        })
2708    }
2709
2710    fn index_preparation_value(expires_at: u64) -> ExactPreparationV1 {
2711        ExactPreparationV1::Index {
2712            locator: Locator {
2713                authority: "catalog_confirmation".to_owned(),
2714                value: "logical-clock-preparation".to_owned(),
2715            },
2716            expires_at: wire_u64(expires_at),
2717            semantic_digest: digest('8'),
2718            resource_spec: Box::new(IndexResourceSpec {
2719                format_version: u32::from(lodestone_wire::LODESTONE_API_INDEX_RESOURCE_SPEC_FORMAT_VERSION),
2720                revision: wire_u64(1),
2721                initial_shards: 1,
2722                target_shard_bytes: wire_u64(1),
2723                durability: lodestone_wire::ResourceDurabilityClass::RegionalObject,
2724                storage_class: lodestone_wire::ResourceStorageClass::Standard,
2725                ram_admission_weight: 1,
2726                ram_hard_max_bytes: wire_u64(1),
2727                nvme_admission_weight: 1,
2728                nvme_hard_max_bytes: wire_u64(1),
2729                refresh_objective_millis: 1,
2730                memtable_hard_max_bytes: wire_u64(1),
2731                seal_parallelism: 1,
2732                merge_parallelism: 1,
2733                vector: lodestone_wire::VectorResourcePolicy {
2734                    quality: lodestone_wire::VectorQualityObjective::Balanced,
2735                    indexing: lodestone_wire::VectorIndexingObjective::Balanced,
2736                    residency: None,
2737                    calibration: None,
2738                },
2739                lifecycle_binding: None,
2740                encryption_required: true,
2741                resolver_capability: None,
2742            }),
2743        }
2744    }
2745
2746    #[test]
2747    fn retained_event_identity_preserves_the_catalog_submit_kind() {
2748        assert_eq!(create_index_action_kind(None), "create_index");
2749        assert_eq!(create_index_action_kind(Some(false)), "create_index");
2750        assert_eq!(create_index_action_kind(Some(true)), "create_retained_append_log_index");
2751    }
2752
2753    #[test]
2754    fn catalog_recovery_replays_only_an_exact_prepared_or_pending_identity() {
2755        assert!(catalog_resolution_requires_exact_replay("pending", None));
2756        assert!(catalog_resolution_requires_exact_replay(
2757            "prepared",
2758            Some(ExactSubmissionDispositionV1::Indeterminate),
2759        ));
2760        assert!(catalog_resolution_requires_exact_replay(
2761            "prepared",
2762            Some(ExactSubmissionDispositionV1::Submitted),
2763        ));
2764        assert!(!catalog_resolution_requires_exact_replay("prepared", None));
2765        assert!(!catalog_resolution_requires_exact_replay(
2766            "prepared",
2767            Some(ExactSubmissionDispositionV1::NotSubmitted),
2768        ));
2769        assert!(!catalog_resolution_requires_exact_replay(
2770            "expired_unknown",
2771            Some(ExactSubmissionDispositionV1::Indeterminate),
2772        ));
2773    }
2774
2775    #[test]
2776    fn catalog_logical_expiry_never_clamps_the_client_unix_deadline() -> Result<(), Box<dyn Error>> {
2777        const CLIENT_NOW: u64 = 1_793_100_501_940;
2778        const EFFECT_DEADLINE: u64 = CLIENT_NOW + 10_000;
2779        const AUTHORITY_LOGICAL_EXPIRY: u64 = 8_390_001;
2780
2781        let intent = exact_intent(
2782            ExactMutationKindV1::Index,
2783            digest('b'),
2784            identity('c'),
2785            identity('d'),
2786            None,
2787            digest('e'),
2788            EFFECT_DEADLINE,
2789        );
2790        let mut prepared = recovery_from_intent(&intent);
2791        reduce_transition(
2792            &mut prepared,
2793            &ExactMutationTransition::ClockObserved {
2794                unix_millis: wire_u64(CLIENT_NOW),
2795            },
2796        )?;
2797        reduce_transition(
2798            &mut prepared,
2799            &ExactMutationTransition::Prepared {
2800                preparation: index_preparation_value(AUTHORITY_LOGICAL_EXPIRY),
2801            },
2802        )?;
2803
2804        let restarted: ExactWorkflowRecoveryV2 = serde_json::from_slice(&serde_json::to_vec(&prepared)?)?;
2805        assert_eq!(index_preparation(&restarted)?.1, wire_u64(AUTHORITY_LOGICAL_EXPIRY));
2806        assert!(may_start_catalog_submission(
2807            &restarted,
2808            CLIENT_NOW + 5_000,
2809            EFFECT_DEADLINE,
2810        ));
2811        assert!(!may_start_catalog_submission(
2812            &restarted,
2813            EFFECT_DEADLINE,
2814            EFFECT_DEADLINE,
2815        ));
2816
2817        let mut uncertain = restarted.clone();
2818        reduce_transition(&mut uncertain, &ExactMutationTransition::SubmissionStarted)?;
2819        reduce_transition(
2820            &mut uncertain,
2821            &ExactMutationTransition::SubmissionObserved {
2822                disposition: ExactSubmissionDispositionV1::Indeterminate,
2823            },
2824        )?;
2825        assert!(!may_start_catalog_submission(
2826            &uncertain,
2827            CLIENT_NOW + 5_000,
2828            EFFECT_DEADLINE,
2829        ));
2830
2831        let mut proven_not_submitted = restarted;
2832        reduce_transition(&mut proven_not_submitted, &ExactMutationTransition::SubmissionStarted)?;
2833        reduce_transition(
2834            &mut proven_not_submitted,
2835            &ExactMutationTransition::SubmissionObserved {
2836                disposition: ExactSubmissionDispositionV1::NotSubmitted,
2837            },
2838        )?;
2839        assert!(may_start_catalog_submission(
2840            &proven_not_submitted,
2841            CLIENT_NOW + 5_000,
2842            EFFECT_DEADLINE,
2843        ));
2844        Ok(())
2845    }
2846
2847    #[test]
2848    fn recovery_refuses_changed_request_or_scope_and_retains_first_deadline() -> Result<(), Box<dyn Error>> {
2849        let intent = exact_intent(
2850            ExactMutationKindV1::Append,
2851            digest('b'),
2852            identity('c'),
2853            identity('d'),
2854            Some(resource()),
2855            digest('e'),
2856            100,
2857        );
2858        let recovery = recovery_from_intent(&intent);
2859        validate_recovery_matches(&recovery, &intent)?;
2860        assert_eq!(recovery_effect_deadline(&recovery)?, 100);
2861        let mut changed = intent.clone();
2862        changed.collection_id = identity('f');
2863        assert!(matches!(
2864            validate_recovery_matches(&recovery, &changed),
2865            Err(ExactWorkflowError::InvalidRecovery)
2866        ));
2867        changed = intent.clone();
2868        changed.request_sha256 = digest('f');
2869        assert!(matches!(
2870            validate_recovery_matches(&recovery, &changed),
2871            Err(ExactWorkflowError::InvalidRecovery)
2872        ));
2873        Ok(())
2874    }
2875
2876    #[test]
2877    fn reducer_never_regresses_clock_or_replaces_possible_submission() -> Result<(), Box<dyn Error>> {
2878        let intent = exact_intent(
2879            ExactMutationKindV1::Append,
2880            digest('b'),
2881            identity('c'),
2882            identity('d'),
2883            Some(resource()),
2884            digest('e'),
2885            100,
2886        );
2887        let mut state = recovery_from_intent(&intent);
2888        reduce_transition(
2889            &mut state,
2890            &ExactMutationTransition::ClockObserved {
2891                unix_millis: wire_u64(10),
2892            },
2893        )?;
2894        assert!(
2895            reduce_transition(
2896                &mut state,
2897                &ExactMutationTransition::ClockObserved {
2898                    unix_millis: wire_u64(9),
2899                },
2900            )
2901            .is_err()
2902        );
2903        let preparation = ExactPreparationV1::Append(RetainedAppendBulkOutcome {
2904            batch_id: digest('e'),
2905            index: resource(),
2906            items: vec![RetainedAppendItemOutcome {
2907                item_action_id: digest('f'),
2908                locator: Some(Locator {
2909                    authority: "retained_append".to_owned(),
2910                    value: "01".to_owned(),
2911                }),
2912                semantic_digest: Some(digest('1')),
2913                state: RetainedAppendState::Prepared,
2914                retry_safety: RetainedAppendRetrySafety::Safe,
2915                sequence: None,
2916                primary_term: None,
2917                error: None,
2918            }],
2919            complete: true,
2920            achievement: None,
2921        });
2922        reduce_transition(
2923            &mut state,
2924            &ExactMutationTransition::Prepared {
2925                preparation: preparation.clone(),
2926            },
2927        )?;
2928        reduce_transition(&mut state, &ExactMutationTransition::SubmissionStarted)?;
2929        reduce_transition(
2930            &mut state,
2931            &ExactMutationTransition::SubmissionObserved {
2932                disposition: ExactSubmissionDispositionV1::Indeterminate,
2933            },
2934        )?;
2935        assert!(reduce_transition(&mut state, &ExactMutationTransition::Prepared { preparation },).is_err());
2936        Ok(())
2937    }
2938
2939    #[test]
2940    fn reducer_refuses_revision_overflow_without_changing_state() {
2941        let intent = exact_intent(
2942            ExactMutationKindV1::Append,
2943            digest('b'),
2944            identity('c'),
2945            identity('d'),
2946            Some(resource()),
2947            digest('e'),
2948            100,
2949        );
2950        let mut state = recovery_from_intent(&intent);
2951        state.revision = MAX_SAFE_REVISION;
2952        let unchanged = state.clone();
2953        assert!(
2954            reduce_transition(
2955                &mut state,
2956                &ExactMutationTransition::ClockObserved {
2957                    unix_millis: wire_u64(1),
2958                },
2959            )
2960            .is_err()
2961        );
2962        assert_eq!(state, unchanged);
2963    }
2964
2965    #[test]
2966    fn not_submitted_append_outcome_remains_recovery_resumable() -> Result<(), Box<dyn Error>> {
2967        let request = AppendExactRequestV1 {
2968            batch_id: digest('a'),
2969            index: resource(),
2970            items: vec![RetainedAppendItem {
2971                item_action_id: digest('b'),
2972                document_id: "event-1".to_owned(),
2973                record: lodestone_wire::NativeRecord::Document {
2974                    source: lodestone_wire::RawJson {
2975                        bytes_base64: "e30=".to_owned(),
2976                    },
2977                },
2978            }],
2979            observe_committed: true,
2980        };
2981        let outcome = RetainedAppendBulkOutcome {
2982            batch_id: request.batch_id.clone(),
2983            index: request.index.clone(),
2984            items: vec![RetainedAppendItemOutcome {
2985                item_action_id: request.items[0].item_action_id.clone(),
2986                locator: Some(Locator {
2987                    authority: "retained_append".to_owned(),
2988                    value: "01".to_owned(),
2989                }),
2990                semantic_digest: Some(digest('c')),
2991                state: RetainedAppendState::NotSubmittedRetryable,
2992                retry_safety: RetainedAppendRetrySafety::Safe,
2993                sequence: None,
2994                primary_term: None,
2995                error: None,
2996            }],
2997            complete: false,
2998            achievement: None,
2999        };
3000
3001        let result = append_terminal_result(&request, &outcome, false).ok_or("not-submitted observation absent")?;
3002        assert_eq!(result.outcome, ExactMutationOutcomeV1::NotSubmitted);
3003        assert!(!terminal_outcome(result.outcome));
3004        Ok(())
3005    }
3006
3007    #[test]
3008    fn append_recovery_redacts_server_error_messages() {
3009        let outcome = RetainedAppendBulkOutcome {
3010            batch_id: digest('a'),
3011            index: resource(),
3012            items: vec![RetainedAppendItemOutcome {
3013                item_action_id: digest('b'),
3014                locator: Some(Locator {
3015                    authority: "retained_append".to_owned(),
3016                    value: "01".to_owned(),
3017                }),
3018                semantic_digest: Some(digest('c')),
3019                state: RetainedAppendState::Prepared,
3020                retry_safety: RetainedAppendRetrySafety::Safe,
3021                sequence: None,
3022                primary_term: None,
3023                error: None,
3024            }],
3025            complete: true,
3026            achievement: None,
3027        };
3028        assert!(redact_append_outcome(outcome).items[0].error.is_none());
3029    }
3030
3031    #[tokio::test]
3032    async fn caller_owned_cas_converges_two_replicas_across_every_phase() -> Result<(), Box<dyn Error>> {
3033        let store: Arc<dyn ExactWorkflowRecoveryStore> = Arc::new(PostgresShapedRecoveryStore::default());
3034        let intent = exact_intent(
3035            ExactMutationKindV1::Append,
3036            digest('b'),
3037            identity('c'),
3038            identity('d'),
3039            Some(resource()),
3040            digest('e'),
3041            10_000,
3042        );
3043        let options = ExactWorkflowOptions::new(10_000)
3044            .with_observation_deadline_unix_millis(20_000)
3045            .with_recovery_store(Arc::clone(&store));
3046        let (left_session, mut left) = open_session(&intent, &options).await?;
3047        let (right_session, mut right) = open_session(&intent, &options).await?;
3048        assert_eq!(left.revision, 0);
3049        assert_eq!(right.revision, 0);
3050
3051        assert!(
3052            left_session
3053                .record(
3054                    &mut left,
3055                    ExactMutationTransition::ClockObserved {
3056                        unix_millis: wire_u64(1),
3057                    },
3058                )
3059                .await?
3060        );
3061        assert!(
3062            !right_session
3063                .record(
3064                    &mut right,
3065                    ExactMutationTransition::ClockObserved {
3066                        unix_millis: wire_u64(1),
3067                    },
3068                )
3069                .await?
3070        );
3071
3072        let preparation = append_preparation_value();
3073        assert!(
3074            left_session
3075                .record(
3076                    &mut left,
3077                    ExactMutationTransition::Prepared {
3078                        preparation: preparation.clone(),
3079                    },
3080                )
3081                .await?
3082        );
3083        assert!(
3084            !right_session
3085                .record(&mut right, ExactMutationTransition::Prepared { preparation },)
3086                .await?
3087        );
3088
3089        assert!(
3090            left_session
3091                .record(&mut left, ExactMutationTransition::SubmissionStarted)
3092                .await?
3093        );
3094        assert!(
3095            !right_session
3096                .record(&mut right, ExactMutationTransition::SubmissionStarted)
3097                .await?
3098        );
3099
3100        let observation = ExactMutationTransition::SubmissionObserved {
3101            disposition: ExactSubmissionDispositionV1::Resolved,
3102        };
3103        assert!(left_session.record(&mut left, observation.clone()).await?);
3104        assert!(!right_session.record(&mut right, observation).await?);
3105
3106        let result = ExactTerminalResultV1::Append(AppendExactResultV1 {
3107            outcome: ExactMutationOutcomeV1::Resolved,
3108            batch_id: digest('e'),
3109            index: resource(),
3110            items: Vec::new(),
3111            visibility: Vec::new(),
3112        });
3113        assert!(
3114            left_session
3115                .record(&mut left, ExactMutationTransition::Terminal { result: result.clone() },)
3116                .await?
3117        );
3118        let losing = AppendExactResultV1 {
3119            outcome: ExactMutationOutcomeV1::Conflict,
3120            batch_id: digest('e'),
3121            index: resource(),
3122            items: Vec::new(),
3123            visibility: Vec::new(),
3124        };
3125        let adopted = finish_append(&right_session, &mut right, losing).await?;
3126        assert_eq!(adopted.result.outcome, ExactMutationOutcomeV1::Resolved);
3127        assert_eq!(left, right);
3128        assert_eq!(left.phase, ExactWorkflowPhaseV2::Terminal);
3129        assert_eq!(left.revision, 5);
3130        assert_eq!(store.load().await?, Some(left));
3131        Ok(())
3132    }
3133
3134    #[test]
3135    fn rust_recovery_v2_matches_the_cross_language_fixture() -> Result<(), Box<dyn Error>> {
3136        let fixture: serde_json::Value = serde_json::from_str(include_str!(
3137            "../../../clients/native/fixtures/lodestone-exact-workflow-recovery-v2.json"
3138        ))?;
3139        if fixture.get("format_version").and_then(serde_json::Value::as_u64) != Some(2) {
3140            return Err("exact recovery fixture format differs".into());
3141        }
3142        let values = fixture
3143            .get("states")
3144            .and_then(serde_json::Value::as_array)
3145            .ok_or("exact recovery fixture states are absent")?;
3146        let states: Vec<ExactWorkflowRecoveryV2> = values
3147            .iter()
3148            .cloned()
3149            .map(serde_json::from_value)
3150            .collect::<Result<_, _>>()?;
3151        assert_eq!(states.len(), 6);
3152        for state in &states {
3153            validate_recovery_shape(state)?;
3154        }
3155        for pair in states.windows(2) {
3156            let [current, next] = pair else {
3157                return Err("exact recovery fixture window differs".into());
3158            };
3159            let transition = transition_between(current, next)?;
3160            let mut reduced = current.clone();
3161            reduce_transition(&mut reduced, &transition)?;
3162            assert_eq!(&reduced, next);
3163        }
3164        assert_eq!(
3165            states.last().map(|value| value.phase),
3166            Some(ExactWorkflowPhaseV2::Terminal)
3167        );
3168        Ok(())
3169    }
3170
3171    #[tokio::test]
3172    async fn store_values_fail_closed_by_format_shape_and_intent() -> Result<(), Box<dyn Error>> {
3173        let intent = exact_intent(
3174            ExactMutationKindV1::Append,
3175            digest('b'),
3176            identity('c'),
3177            identity('d'),
3178            Some(resource()),
3179            digest('e'),
3180            10_000,
3181        );
3182        let initial = recovery_from_intent(&intent);
3183
3184        let mut future = initial.clone();
3185        future.format_version = EXACT_MUTATION_FORMAT_VERSION.saturating_add(1);
3186        let future_store: Arc<dyn ExactWorkflowRecoveryStore> = Arc::new(PostgresShapedRecoveryStore {
3187            row: Mutex::new(Some(future)),
3188        });
3189        let future_result = open_session(
3190            &intent,
3191            &ExactWorkflowOptions::new(10_000).with_recovery_store(future_store),
3192        )
3193        .await;
3194        assert!(matches!(
3195            future_result,
3196            Err(ExactWorkflowError::Store {
3197                source: ExactWorkflowStoreError::UnsupportedFormat { .. },
3198                ..
3199            })
3200        ));
3201
3202        let mut malformed = initial.clone();
3203        malformed.phase = ExactWorkflowPhaseV2::Prepared;
3204        let malformed_store: Arc<dyn ExactWorkflowRecoveryStore> = Arc::new(PostgresShapedRecoveryStore {
3205            row: Mutex::new(Some(malformed)),
3206        });
3207        let malformed_result = open_session(
3208            &intent,
3209            &ExactWorkflowOptions::new(10_000).with_recovery_store(malformed_store),
3210        )
3211        .await;
3212        assert!(matches!(
3213            malformed_result,
3214            Err(ExactWorkflowError::Store {
3215                source: ExactWorkflowStoreError::Malformed,
3216                ..
3217            })
3218        ));
3219
3220        let mut mismatched = initial;
3221        mismatched.request_sha256 = digest('9');
3222        let mismatch_store: Arc<dyn ExactWorkflowRecoveryStore> = Arc::new(PostgresShapedRecoveryStore {
3223            row: Mutex::new(Some(mismatched)),
3224        });
3225        let mismatch_result = open_session(
3226            &intent,
3227            &ExactWorkflowOptions::new(10_000).with_recovery_store(mismatch_store),
3228        )
3229        .await;
3230        assert!(matches!(
3231            mismatch_result,
3232            Err(ExactWorkflowError::Store {
3233                source: ExactWorkflowStoreError::IntentMismatch,
3234                ..
3235            })
3236        ));
3237
3238        let unavailable: Arc<dyn ExactWorkflowRecoveryStore> = Arc::new(UnavailableRecoveryStore);
3239        let unavailable_result = open_session(
3240            &intent,
3241            &ExactWorkflowOptions::new(10_000).with_recovery_store(unavailable),
3242        )
3243        .await;
3244        assert!(matches!(
3245            unavailable_result,
3246            Err(ExactWorkflowError::Store {
3247                source: ExactWorkflowStoreError::Unavailable,
3248                last_durable_recovery: None,
3249            })
3250        ));
3251        Ok(())
3252    }
3253
3254    #[tokio::test]
3255    async fn store_conflicts_and_cancellation_are_bounded_with_recovery() -> Result<(), Box<dyn Error>> {
3256        let intent = exact_intent(
3257            ExactMutationKindV1::Append,
3258            digest('b'),
3259            identity('c'),
3260            identity('d'),
3261            Some(resource()),
3262            digest('e'),
3263            u64::MAX,
3264        );
3265        let store: Arc<dyn ExactWorkflowRecoveryStore> = Arc::new(AdvancingConflictStore {
3266            row: Mutex::new(recovery_from_intent(&intent)),
3267        });
3268        let options = ExactWorkflowOptions::new(u64::MAX)
3269            .with_observation_deadline_unix_millis(u64::MAX)
3270            .with_store_conflict_limit(0)
3271            .with_recovery_store(store);
3272        let (session, mut recovery) = open_session(&intent, &options).await?;
3273        let conflict = session
3274            .record(
3275                &mut recovery,
3276                ExactMutationTransition::Prepared {
3277                    preparation: append_preparation_value(),
3278                },
3279            )
3280            .await;
3281        assert!(matches!(
3282            conflict,
3283            Err(ExactWorkflowError::StoreConflictBudgetExhausted {
3284                last_durable_recovery
3285            }) if last_durable_recovery.revision == 1
3286        ));
3287
3288        let cancellation = CallCancellation::new();
3289        cancellation.cancel();
3290        let store: Arc<dyn ExactWorkflowRecoveryStore> = Arc::new(PostgresShapedRecoveryStore::default());
3291        let options = ExactWorkflowOptions::new(u64::MAX)
3292            .with_cancellation(cancellation)
3293            .with_recovery_store(store);
3294        let (session, mut recovery) = open_session(&intent, &options).await?;
3295        let cancelled = session
3296            .record(
3297                &mut recovery,
3298                ExactMutationTransition::ClockObserved {
3299                    unix_millis: wire_u64(1),
3300                },
3301            )
3302            .await;
3303        assert!(matches!(
3304            cancelled,
3305            Err(ExactWorkflowError::Cancelled {
3306                last_durable_recovery
3307            }) if last_durable_recovery.revision == 0
3308        ));
3309        Ok(())
3310    }
3311
3312    #[tokio::test]
3313    async fn durable_visibility_polling_does_not_exhaust_recovery_records() -> Result<(), Box<dyn Error>> {
3314        let intent = exact_intent(
3315            ExactMutationKindV1::Append,
3316            digest('b'),
3317            identity('c'),
3318            identity('d'),
3319            Some(resource()),
3320            digest('e'),
3321            u64::MAX,
3322        );
3323        let store: Arc<dyn ExactWorkflowRecoveryStore> = Arc::new(PostgresShapedRecoveryStore::default());
3324        let clock: Arc<dyn ExactWorkflowClock> = Arc::new(AdvancingClock { next: Mutex::new(1) });
3325        let options = ExactWorkflowOptions::new(u64::MAX)
3326            .with_clock(clock)
3327            .with_recovery_store(Arc::clone(&store));
3328        let (session, mut recovery) = open_session(&intent, &options).await?;
3329
3330        assert_eq!(observe_clock(&session, &mut recovery, &options).await?, 1);
3331        assert!(
3332            session
3333                .record(
3334                    &mut recovery,
3335                    ExactMutationTransition::Prepared {
3336                        preparation: append_preparation_value(),
3337                    },
3338                )
3339                .await?
3340        );
3341        assert!(
3342            session
3343                .record(&mut recovery, ExactMutationTransition::SubmissionStarted)
3344                .await?
3345        );
3346        assert!(
3347            session
3348                .record(
3349                    &mut recovery,
3350                    ExactMutationTransition::SubmissionObserved {
3351                        disposition: ExactSubmissionDispositionV1::Resolved,
3352                    },
3353                )
3354                .await?
3355        );
3356        assert_eq!(recovery.revision, 4);
3357
3358        for expected_now in 2..=301 {
3359            assert_eq!(observe_clock(&session, &mut recovery, &options).await?, expected_now);
3360            record_resolution_observation(&session, &mut recovery, ExactSubmissionDispositionV1::Resolved).await?;
3361        }
3362        assert_eq!(recovery.revision, 4);
3363        assert_eq!(store.load().await?.map(|stored| stored.revision), Some(4));
3364        Ok(())
3365    }
3366
3367    #[tokio::test]
3368    async fn durable_resume_retains_the_first_effect_deadline() -> Result<(), Box<dyn Error>> {
3369        let mut suffix = [0_u8; 8];
3370        getrandom::fill(&mut suffix)?;
3371        let parent =
3372            std::fs::canonicalize(std::env::temp_dir())?.join(format!("lodestone-exact-{}", lower_hex(&suffix)));
3373        std::fs::create_dir(&parent)?;
3374        std::fs::set_permissions(&parent, std::fs::Permissions::from_mode(0o700))?;
3375        let path = parent.join("journal");
3376        let first_intent = exact_intent(
3377            ExactMutationKindV1::Append,
3378            digest('b'),
3379            identity('c'),
3380            identity('d'),
3381            Some(resource()),
3382            digest('e'),
3383            100,
3384        );
3385        {
3386            let store = Arc::new(FilesystemExactWorkflowRecoveryStore::new(path.clone()));
3387            let first = ExactWorkflowOptions::new(100).with_recovery_store(store);
3388            let (session, recovery) = open_session(&first_intent, &first).await?;
3389            assert_eq!(recovery_effect_deadline(&recovery)?, 100);
3390            drop(session);
3391        }
3392
3393        let resumed_intent = ExactMutationIntent {
3394            effect_deadline_unix_millis: wire_u64(999),
3395            ..first_intent
3396        };
3397        let store = Arc::new(FilesystemExactWorkflowRecoveryStore::new(path));
3398        let resumed = ExactWorkflowOptions::new(999).with_recovery_store(store);
3399        let (session, recovery) = open_session(&resumed_intent, &resumed).await?;
3400        assert_eq!(recovery_effect_deadline(&recovery)?, 100);
3401        drop(session);
3402        std::fs::remove_dir_all(&parent)?;
3403        Ok(())
3404    }
3405
3406    #[tokio::test]
3407    async fn filesystem_store_restarts_at_each_v2_durability_boundary() -> Result<(), Box<dyn Error>> {
3408        let mut suffix = [0_u8; 8];
3409        getrandom::fill(&mut suffix)?;
3410        let parent =
3411            std::fs::canonicalize(std::env::temp_dir())?.join(format!("lodestone-exact-v2-{}", lower_hex(&suffix)));
3412        std::fs::create_dir(&parent)?;
3413        std::fs::set_permissions(&parent, std::fs::Permissions::from_mode(0o700))?;
3414        let path = parent.join("journal");
3415        let intent = exact_intent(
3416            ExactMutationKindV1::Append,
3417            digest('b'),
3418            identity('c'),
3419            identity('d'),
3420            Some(resource()),
3421            digest('e'),
3422            10_000,
3423        );
3424        let mut state = recovery_from_intent(&intent);
3425        let store = FilesystemExactWorkflowRecoveryStore::new(path.clone());
3426        store.compare_exchange(None, state.clone()).await?;
3427        drop(store);
3428
3429        let transitions = [
3430            ExactMutationTransition::Prepared {
3431                preparation: append_preparation_value(),
3432            },
3433            ExactMutationTransition::SubmissionStarted,
3434            ExactMutationTransition::SubmissionObserved {
3435                disposition: ExactSubmissionDispositionV1::Indeterminate,
3436            },
3437            ExactMutationTransition::SubmissionObserved {
3438                disposition: ExactSubmissionDispositionV1::Resolved,
3439            },
3440            ExactMutationTransition::Terminal {
3441                result: ExactTerminalResultV1::Append(AppendExactResultV1 {
3442                    outcome: ExactMutationOutcomeV1::Resolved,
3443                    batch_id: digest('e'),
3444                    index: resource(),
3445                    items: Vec::new(),
3446                    visibility: Vec::new(),
3447                }),
3448            },
3449        ];
3450        for transition in transitions {
3451            let store = FilesystemExactWorkflowRecoveryStore::new(path.clone());
3452            assert_eq!(store.load().await?, Some(state.clone()));
3453            let mut next = state.clone();
3454            reduce_transition(&mut next, &transition)?;
3455            store.compare_exchange(Some(state.revision), next.clone()).await?;
3456            state = next;
3457            drop(store);
3458        }
3459        let store = FilesystemExactWorkflowRecoveryStore::new(path);
3460        let loaded = store.load().await?.ok_or("filesystem recovery disappeared")?;
3461        assert_eq!(loaded.phase, ExactWorkflowPhaseV2::Terminal);
3462        assert_eq!(loaded.caller_action_id, digest('e'));
3463        assert_eq!(
3464            append_preparation(&loaded)?.items[0]
3465                .locator
3466                .as_ref()
3467                .map(|value| value.value.as_str()),
3468            Some("01")
3469        );
3470        drop(store);
3471        std::fs::remove_dir_all(&parent)?;
3472        Ok(())
3473    }
3474}