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        let expiry = parse_u64(&expires_at).ok_or(ExactWorkflowError::InvalidRecovery)?;
1308        let submit_deadline = options.effect_deadline_unix_millis.min(expiry);
1309        let submission = SubmitCatalogActionRequest {
1310            locator: locator.clone(),
1311            semantic_digest,
1312            action_kind: create_index_action_kind(request.retained_event_identity).to_owned(),
1313            name: request.name.clone(),
1314            mapping: Some(request.mapping.clone()),
1315            resource_spec: Some(resource_spec),
1316            expected_incarnation: None,
1317        };
1318        let may_submit = recovery.phase == ExactWorkflowPhaseV2::Prepared
1319            || recovery.submission_disposition == Some(ExactSubmissionDispositionV1::NotSubmitted);
1320        if may_submit && now < submit_deadline {
1321            let owns_submission = session
1322                .record(&mut recovery, ExactMutationTransition::SubmissionStarted)
1323                .await?;
1324            if let Some(ExactTerminalResultV1::Index(result)) = &recovery.terminal {
1325                return Ok(execution(result.clone(), recovery));
1326            }
1327            if owns_submission {
1328                match self
1329                    .submit_catalog_action(submission.clone(), effect_options(now, submit_deadline, &options)?)
1330                    .await
1331                {
1332                    Ok(value) => {
1333                        if value.locator != locator {
1334                            return Err(ExactWorkflowError::CoreShape);
1335                        }
1336                        session
1337                            .record(
1338                                &mut recovery,
1339                                ExactMutationTransition::SubmissionObserved {
1340                                    disposition: exact_submission_disposition(&value.mutation_outcome),
1341                                },
1342                            )
1343                            .await?;
1344                        if let Some(ExactTerminalResultV1::Index(result)) = &recovery.terminal {
1345                            return Ok(execution(result.clone(), recovery));
1346                        }
1347                    }
1348                    Err(error) => {
1349                        let disposition = ExactSubmissionDispositionV1::from(error.submission_disposition());
1350                        session
1351                            .record(
1352                                &mut recovery,
1353                                ExactMutationTransition::SubmissionObserved { disposition },
1354                            )
1355                            .await?;
1356                        if let Some(ExactTerminalResultV1::Index(result)) = &recovery.terminal {
1357                            return Ok(execution(result.clone(), recovery));
1358                        }
1359                        if recovery.submission_disposition == Some(ExactSubmissionDispositionV1::NotSubmitted) {
1360                            return Ok(execution(
1361                                index_status(&request, &recovery, ExactMutationOutcomeV1::NotSubmitted),
1362                                recovery,
1363                            ));
1364                        }
1365                    }
1366                }
1367            }
1368        }
1369        let Some(resolve_options) = resolution_options(now, &options)? else {
1370            return Ok(execution(
1371                index_status(&request, &recovery, ExactMutationOutcomeV1::Indeterminate),
1372                recovery,
1373            ));
1374        };
1375        let resolve_options = resolve_options
1376            .with_index_name(request.name.clone())
1377            .map_err(ExactWorkflowError::Client)?;
1378        let mut value = match self
1379            .resolve_catalog_action(
1380                ResolveCatalogActionRequest {
1381                    locator: locator.clone(),
1382                },
1383                resolve_options.clone(),
1384            )
1385            .await
1386        {
1387            Ok(value) => value,
1388            Err(_) => {
1389                return Ok(execution(
1390                    index_status(&request, &recovery, ExactMutationOutcomeV1::Indeterminate),
1391                    recovery,
1392                ));
1393            }
1394        };
1395        if value.locator != locator {
1396            return Err(ExactWorkflowError::CoreShape);
1397        }
1398        let exact_replay =
1399            catalog_resolution_requires_exact_replay(value.state.as_str(), recovery.submission_disposition);
1400        if exact_replay {
1401            // The owner has proved that this exact effect is already durably
1402            // admitted. Calling the same endpoint resumes resolution; it does
1403            // not authorize a first submission or extend the effect identity.
1404            match self.submit_catalog_action(submission, resolve_options.clone()).await {
1405                Ok(replayed) if replayed.locator == locator => {}
1406                Ok(_) => return Err(ExactWorkflowError::CoreShape),
1407                Err(error) if error.submission_disposition() == SubmissionDisposition::NotSubmitted => {
1408                    return Ok(execution(
1409                        index_status(&request, &recovery, ExactMutationOutcomeV1::NotSubmitted),
1410                        recovery,
1411                    ));
1412                }
1413                Err(_) => {
1414                    return Ok(execution(
1415                        index_status(&request, &recovery, ExactMutationOutcomeV1::Indeterminate),
1416                        recovery,
1417                    ));
1418                }
1419            }
1420            value = match self
1421                .resolve_catalog_action(
1422                    ResolveCatalogActionRequest {
1423                        locator: locator.clone(),
1424                    },
1425                    resolve_options,
1426                )
1427                .await
1428            {
1429                Ok(value) => value,
1430                Err(_) => {
1431                    return Ok(execution(
1432                        index_status(&request, &recovery, ExactMutationOutcomeV1::Indeterminate),
1433                        recovery,
1434                    ));
1435                }
1436            };
1437            if value.locator != locator {
1438                return Err(ExactWorkflowError::CoreShape);
1439            }
1440        }
1441        let outcome = match value.state.as_str() {
1442            "applied" => ExactMutationOutcomeV1::Resolved,
1443            "failed" => ExactMutationOutcomeV1::Conflict,
1444            "expired_unknown" => ExactMutationOutcomeV1::KnownRetired,
1445            "prepared" => ExactMutationOutcomeV1::NotSubmitted,
1446            "pending" => ExactMutationOutcomeV1::Indeterminate,
1447            _ => return Err(ExactWorkflowError::CoreShape),
1448        };
1449        record_resolution_observation(&session, &mut recovery, resolution_disposition(outcome)).await?;
1450        if let Some(ExactTerminalResultV1::Index(result)) = &recovery.terminal {
1451            return Ok(execution(result.clone(), recovery));
1452        }
1453        let mut result = ReconcileIndexExactResultV1 {
1454            outcome,
1455            name: request.name,
1456            locator: Some(value.locator),
1457            resource: value.resource,
1458            state: value.state,
1459        };
1460        if outcome == ExactMutationOutcomeV1::Resolved {
1461            let Some(verify_options) = resolution_options(now, &options)? else {
1462                result.outcome = ExactMutationOutcomeV1::Indeterminate;
1463                return Ok(execution(result, recovery));
1464            };
1465            let existing = match self
1466                .get_index(
1467                    GetIndexRequest {
1468                        name: result.name.clone(),
1469                        expected_incarnation: result.resource.as_ref().map(|value| value.incarnation.clone()),
1470                    },
1471                    verify_options,
1472                )
1473                .await
1474            {
1475                Ok(value) => value.resource,
1476                Err(_) => {
1477                    result.outcome = ExactMutationOutcomeV1::Indeterminate;
1478                    return Ok(execution(result, recovery));
1479                }
1480            };
1481            if existing.current_mapping != request.mapping
1482                || request.resource_spec.as_ref() != Some(&existing.resource_spec)
1483                || existing.summary.retained_event_identity != request.retained_event_identity.unwrap_or(false)
1484            {
1485                result.outcome = ExactMutationOutcomeV1::Conflict;
1486                result.state = "definition_conflict".to_owned();
1487            } else if existing.summary.state != "active" {
1488                result.outcome = ExactMutationOutcomeV1::Indeterminate;
1489                result.state = existing.summary.state;
1490                return Ok(execution(result, recovery));
1491            } else {
1492                result.resource = Some(Resource {
1493                    kind: "index".to_owned(),
1494                    name: existing.summary.name,
1495                    incarnation: existing.summary.incarnation,
1496                });
1497                result.state = "active".to_owned();
1498            }
1499        }
1500        if matches!(
1501            outcome,
1502            ExactMutationOutcomeV1::Indeterminate | ExactMutationOutcomeV1::NotSubmitted
1503        ) {
1504            Ok(execution(result, recovery))
1505        } else {
1506            finish_index(&session, &mut recovery, result).await
1507        }
1508    }
1509
1510    fn exact_profile_scope(&self) -> (Identity, Identity) {
1511        let profile = self.connection_profile().as_wire();
1512        (
1513            profile.expected_cluster_id.clone(),
1514            profile.expected_collection_id.clone(),
1515        )
1516    }
1517}
1518
1519fn catalog_resolution_requires_exact_replay(state: &str, submission: Option<ExactSubmissionDispositionV1>) -> bool {
1520    state == "pending"
1521        || state == "prepared"
1522            && matches!(
1523                submission,
1524                Some(
1525                    ExactSubmissionDispositionV1::Submitted
1526                        | ExactSubmissionDispositionV1::Resolved
1527                        | ExactSubmissionDispositionV1::Indeterminate
1528                )
1529            )
1530}
1531
1532async fn observe_append_visibility(
1533    client: &Client,
1534    request: &AppendExactRequestV1,
1535    mut result: AppendExactResultV1,
1536    options: CallOptions,
1537) -> Result<AppendExactResultV1, ExactWorkflowError> {
1538    let mut visibility = Vec::with_capacity(result.items.len());
1539    for item in &result.items {
1540        let Some(locator) = item.locator.clone() else {
1541            return Err(ExactWorkflowError::CoreShape);
1542        };
1543        match client
1544            .observe_append_committed(
1545                ObserveAppendCommittedRequest {
1546                    index: request.index.clone(),
1547                    locator,
1548                },
1549                options.clone(),
1550            )
1551            .await
1552        {
1553            Ok(value) => visibility.push(value.outcome),
1554            Err(_) => {
1555                result.outcome = ExactMutationOutcomeV1::Indeterminate;
1556                result.visibility = visibility;
1557                return Ok(result);
1558            }
1559        }
1560    }
1561    if visibility
1562        .iter()
1563        .any(|value| value.state != RetainedAppendObservationState::Visible)
1564    {
1565        result.outcome = ExactMutationOutcomeV1::Indeterminate;
1566    }
1567    result.visibility = visibility;
1568    Ok(result)
1569}
1570
1571fn exact_intent(
1572    mutation_kind: ExactMutationKindV1,
1573    request_sha256: Digest,
1574    cluster_id: Identity,
1575    collection_id: Identity,
1576    index: Option<Resource>,
1577    caller_action_id: Digest,
1578    effect_deadline_unix_millis: u64,
1579) -> ExactMutationIntent {
1580    ExactMutationIntent {
1581        format_version: EXACT_MUTATION_FORMAT_VERSION,
1582        mutation_kind,
1583        request_sha256,
1584        cluster_id,
1585        collection_id,
1586        index,
1587        caller_action_id,
1588        effect_deadline_unix_millis: wire_u64(effect_deadline_unix_millis),
1589    }
1590}
1591
1592async fn open_session(
1593    intent: &ExactMutationIntent,
1594    options: &ExactWorkflowOptions,
1595) -> Result<(ExactSession, ExactWorkflowRecoveryV2), ExactWorkflowError> {
1596    intent.validate()?;
1597    if options.effect_deadline_unix_millis == 0 || options.store.is_some() && options.recovery.is_some() {
1598        return Err(ExactWorkflowError::InvalidRecovery);
1599    }
1600    let (session, recovery) = if let Some(store) = &options.store {
1601        let initial = recovery_from_intent(intent);
1602        let loaded = store.load().await.map_err(|error| store_failure(error, None))?;
1603        let recovery = match loaded {
1604            Some(value) => {
1605                validate_store_recovery(&value, intent)?;
1606                value
1607            }
1608            None => match store.compare_exchange(None, initial.clone()).await {
1609                Ok(()) => initial,
1610                Err(ExactWorkflowStoreError::Conflict {
1611                    observed: Some(observed),
1612                }) => {
1613                    let observed = *observed;
1614                    validate_store_recovery(&observed, intent)?;
1615                    observed
1616                }
1617                Err(ExactWorkflowStoreError::Conflict { observed: None }) => {
1618                    return Err(store_failure(ExactWorkflowStoreError::Malformed, None));
1619                }
1620                Err(error) => return Err(store_failure(error, None)),
1621            },
1622        };
1623        let conflict_deadline = options
1624            .observation_deadline_unix_millis
1625            .unwrap_or(options.effect_deadline_unix_millis);
1626        (
1627            ExactSession::Durable {
1628                store: Arc::clone(store),
1629                conflict_limit: options.store_conflict_limit,
1630                deadline_unix_millis: conflict_deadline,
1631                clock: Arc::clone(&options.clock),
1632                cancellation: options.cancellation.clone(),
1633            },
1634            recovery,
1635        )
1636    } else {
1637        let recovery = match &options.recovery {
1638            Some(value) => {
1639                validate_recovery_matches(value, intent)?;
1640                value.clone()
1641            }
1642            None => recovery_from_intent(intent),
1643        };
1644        (ExactSession::Ephemeral, recovery)
1645    };
1646    Ok((session, recovery))
1647}
1648
1649fn recovery_from_intent(intent: &ExactMutationIntent) -> ExactWorkflowRecoveryV2 {
1650    ExactWorkflowRecoveryV2 {
1651        format_version: intent.format_version,
1652        revision: 0,
1653        phase: ExactWorkflowPhaseV2::Initialized,
1654        mutation_kind: intent.mutation_kind,
1655        request_sha256: intent.request_sha256.clone(),
1656        cluster_id: intent.cluster_id.clone(),
1657        collection_id: intent.collection_id.clone(),
1658        index: intent.index.clone(),
1659        caller_action_id: intent.caller_action_id.clone(),
1660        effect_deadline_unix_millis: intent.effect_deadline_unix_millis.clone(),
1661        last_observed_unix_millis: None,
1662        preparation: None,
1663        submission_disposition: None,
1664        terminal: None,
1665    }
1666}
1667
1668fn validate_recovery_matches(
1669    recovery: &ExactWorkflowRecoveryV2,
1670    intent: &ExactMutationIntent,
1671) -> Result<(), ExactWorkflowError> {
1672    let expected = recovery_from_intent(intent);
1673    validate_recovery_shape(recovery)?;
1674    if recovery.format_version != expected.format_version
1675        || recovery.mutation_kind != expected.mutation_kind
1676        || recovery.request_sha256 != expected.request_sha256
1677        || recovery.cluster_id != expected.cluster_id
1678        || recovery.collection_id != expected.collection_id
1679        || recovery.index != expected.index
1680        || recovery.caller_action_id != expected.caller_action_id
1681    {
1682        return Err(ExactWorkflowError::InvalidRecovery);
1683    }
1684    Ok(())
1685}
1686
1687fn reduce_transition(
1688    state: &mut ExactWorkflowRecoveryV2,
1689    transition: &ExactMutationTransition,
1690) -> Result<(), ExactWorkflowError> {
1691    if state.revision >= MAX_SAFE_REVISION || state.phase == ExactWorkflowPhaseV2::Terminal {
1692        return Err(ExactWorkflowError::InvalidRecovery);
1693    }
1694    match transition {
1695        ExactMutationTransition::ClockObserved { unix_millis } => {
1696            let next = parse_u64(unix_millis).ok_or(ExactWorkflowError::InvalidRecovery)?;
1697            if state
1698                .last_observed_unix_millis
1699                .as_ref()
1700                .and_then(parse_u64)
1701                .is_some_and(|prior| next < prior)
1702            {
1703                return Err(ExactWorkflowError::InvalidRecovery);
1704            }
1705            state.last_observed_unix_millis = Some(unix_millis.clone());
1706        }
1707        ExactMutationTransition::Prepared { preparation }
1708            if state.preparation.is_none()
1709                && state.phase == ExactWorkflowPhaseV2::Initialized
1710                && preparation_kind(preparation) == state.mutation_kind =>
1711        {
1712            state.preparation = Some(preparation.clone());
1713            state.phase = ExactWorkflowPhaseV2::Prepared;
1714        }
1715        ExactMutationTransition::SubmissionStarted if state.preparation.is_some() && state.terminal.is_none() => {
1716            if state.phase < ExactWorkflowPhaseV2::SubmissionStarted {
1717                state.phase = ExactWorkflowPhaseV2::SubmissionStarted;
1718            } else if state.submission_disposition == Some(ExactSubmissionDispositionV1::NotSubmitted) {
1719                // INVARIANT: before a retry that follows proof of no effect,
1720                // clear that proof to the conservative may-have-submitted
1721                // state before the transport can observe the request.
1722                state.submission_disposition = Some(ExactSubmissionDispositionV1::Indeterminate);
1723            } else {
1724                return Err(ExactWorkflowError::InvalidRecovery);
1725            }
1726        }
1727        ExactMutationTransition::SubmissionObserved { disposition }
1728            if state.preparation.is_some()
1729                && state.terminal.is_none()
1730                && (state.phase >= ExactWorkflowPhaseV2::SubmissionStarted
1731                    || state.phase == ExactWorkflowPhaseV2::Prepared
1732                        && *disposition == ExactSubmissionDispositionV1::NotSubmitted)
1733                && valid_disposition_successor(state.submission_disposition, *disposition) =>
1734        {
1735            state.submission_disposition = Some(*disposition);
1736            state.phase = ExactWorkflowPhaseV2::SubmissionObserved;
1737        }
1738        ExactMutationTransition::Terminal { result }
1739            if state.terminal.is_none() && terminal_kind(result) == state.mutation_kind =>
1740        {
1741            state.terminal = Some(result.clone());
1742            state.phase = ExactWorkflowPhaseV2::Terminal;
1743        }
1744        _ => return Err(ExactWorkflowError::InvalidRecovery),
1745    }
1746    state.revision = state
1747        .revision
1748        .checked_add(1)
1749        .filter(|value| *value <= MAX_SAFE_REVISION)
1750        .ok_or(ExactWorkflowError::InvalidRecovery)?;
1751    validate_recovery_shape(state)?;
1752    Ok(())
1753}
1754
1755fn validate_recovery_shape(recovery: &ExactWorkflowRecoveryV2) -> Result<(), ExactWorkflowError> {
1756    let preparation_valid = recovery
1757        .preparation
1758        .as_ref()
1759        .is_none_or(|value| valid_preparation(value) && preparation_kind(value) == recovery.mutation_kind);
1760    let terminal_valid = recovery
1761        .terminal
1762        .as_ref()
1763        .is_none_or(|value| valid_terminal(value) && terminal_kind(value) == recovery.mutation_kind);
1764    let phase_valid = match recovery.phase {
1765        ExactWorkflowPhaseV2::Initialized => {
1766            recovery.preparation.is_none() && recovery.submission_disposition.is_none() && recovery.terminal.is_none()
1767        }
1768        ExactWorkflowPhaseV2::Prepared | ExactWorkflowPhaseV2::SubmissionStarted => {
1769            recovery.preparation.is_some() && recovery.submission_disposition.is_none() && recovery.terminal.is_none()
1770        }
1771        ExactWorkflowPhaseV2::SubmissionObserved => {
1772            recovery.preparation.is_some() && recovery.submission_disposition.is_some() && recovery.terminal.is_none()
1773        }
1774        ExactWorkflowPhaseV2::Terminal => recovery.terminal.is_some(),
1775    };
1776    if recovery.format_version != EXACT_MUTATION_FORMAT_VERSION
1777        || recovery.revision > MAX_SAFE_REVISION
1778        || recovery.request_sha256.validate().is_err()
1779        || recovery.cluster_id.validate().is_err()
1780        || recovery.collection_id.validate().is_err()
1781        || recovery.caller_action_id.validate().is_err()
1782        || recovery.index.as_ref().is_some_and(|value| !valid_index(value))
1783        || parse_u64(&recovery.effect_deadline_unix_millis).is_none_or(|value| value == 0)
1784        || recovery
1785            .last_observed_unix_millis
1786            .as_ref()
1787            .is_some_and(|value| parse_u64(value).is_none())
1788        || !preparation_valid
1789        || !terminal_valid
1790        || !phase_valid
1791        || recovery.revision == 0
1792            && (recovery.phase != ExactWorkflowPhaseV2::Initialized || recovery.last_observed_unix_millis.is_some())
1793    {
1794        return Err(ExactWorkflowError::InvalidRecovery);
1795    }
1796    Ok(())
1797}
1798
1799const fn valid_disposition_successor(
1800    current: Option<ExactSubmissionDispositionV1>,
1801    next: ExactSubmissionDispositionV1,
1802) -> bool {
1803    match current {
1804        None | Some(ExactSubmissionDispositionV1::NotSubmitted) => true,
1805        Some(ExactSubmissionDispositionV1::Submitted) => !matches!(next, ExactSubmissionDispositionV1::NotSubmitted),
1806        Some(ExactSubmissionDispositionV1::Indeterminate) => true,
1807        Some(ExactSubmissionDispositionV1::Resolved) => matches!(next, ExactSubmissionDispositionV1::Resolved),
1808        Some(ExactSubmissionDispositionV1::KnownRetired) => {
1809            matches!(next, ExactSubmissionDispositionV1::KnownRetired)
1810        }
1811    }
1812}
1813
1814fn validate_initial_recovery(recovery: &ExactWorkflowRecoveryV2) -> Result<(), ExactWorkflowStoreError> {
1815    validate_recovery_shape(recovery).map_err(|_| ExactWorkflowStoreError::Malformed)?;
1816    if recovery.revision != 0 || recovery.phase != ExactWorkflowPhaseV2::Initialized {
1817        return Err(ExactWorkflowStoreError::Malformed);
1818    }
1819    Ok(())
1820}
1821
1822fn intent_from_recovery(recovery: &ExactWorkflowRecoveryV2) -> ExactMutationIntent {
1823    ExactMutationIntent {
1824        format_version: recovery.format_version,
1825        mutation_kind: recovery.mutation_kind,
1826        request_sha256: recovery.request_sha256.clone(),
1827        cluster_id: recovery.cluster_id.clone(),
1828        collection_id: recovery.collection_id.clone(),
1829        index: recovery.index.clone(),
1830        caller_action_id: recovery.caller_action_id.clone(),
1831        effect_deadline_unix_millis: recovery.effect_deadline_unix_millis.clone(),
1832    }
1833}
1834
1835fn transition_between(
1836    current: &ExactWorkflowRecoveryV2,
1837    next: &ExactWorkflowRecoveryV2,
1838) -> Result<ExactMutationTransition, ExactWorkflowStoreError> {
1839    validate_recovery_shape(current).map_err(|_| ExactWorkflowStoreError::Malformed)?;
1840    validate_recovery_shape(next).map_err(|_| ExactWorkflowStoreError::Malformed)?;
1841    if next.revision
1842        != current
1843            .revision
1844            .checked_add(1)
1845            .ok_or(ExactWorkflowStoreError::Malformed)?
1846        || !same_recovery_intent(current, next)
1847    {
1848        return Err(ExactWorkflowStoreError::Malformed);
1849    }
1850    let retry_started = current.submission_disposition == Some(ExactSubmissionDispositionV1::NotSubmitted)
1851        && next.submission_disposition == Some(ExactSubmissionDispositionV1::Indeterminate)
1852        && current.phase == next.phase;
1853    let candidates = [
1854        next.last_observed_unix_millis
1855            .as_ref()
1856            .filter(|value| current.last_observed_unix_millis.as_ref() != Some(*value))
1857            .map(|value| ExactMutationTransition::ClockObserved {
1858                unix_millis: value.clone(),
1859            }),
1860        next.preparation
1861            .as_ref()
1862            .filter(|value| current.preparation.as_ref() != Some(*value))
1863            .map(|value| ExactMutationTransition::Prepared {
1864                preparation: value.clone(),
1865            }),
1866        (retry_started
1867            || current.phase < ExactWorkflowPhaseV2::SubmissionStarted
1868                && next.phase >= ExactWorkflowPhaseV2::SubmissionStarted)
1869            .then_some(ExactMutationTransition::SubmissionStarted),
1870        next.submission_disposition
1871            .filter(|_| !retry_started)
1872            .map(|disposition| ExactMutationTransition::SubmissionObserved { disposition }),
1873        next.terminal
1874            .as_ref()
1875            .filter(|value| current.terminal.as_ref() != Some(*value))
1876            .map(|value| ExactMutationTransition::Terminal { result: value.clone() }),
1877    ];
1878    let mut matched = None;
1879    for candidate in candidates.into_iter().flatten() {
1880        let mut reduced = current.clone();
1881        if reduce_transition(&mut reduced, &candidate).is_ok() && reduced == *next {
1882            if matched.is_some() {
1883                return Err(ExactWorkflowStoreError::Malformed);
1884            }
1885            matched = Some(candidate);
1886        }
1887    }
1888    matched.ok_or(ExactWorkflowStoreError::Malformed)
1889}
1890
1891fn validate_store_recovery(
1892    recovery: &ExactWorkflowRecoveryV2,
1893    intent: &ExactMutationIntent,
1894) -> Result<(), ExactWorkflowError> {
1895    if recovery.format_version != EXACT_MUTATION_FORMAT_VERSION {
1896        return Err(store_failure(
1897            ExactWorkflowStoreError::UnsupportedFormat {
1898                observed: recovery.format_version,
1899            },
1900            Some(recovery.clone()),
1901        ));
1902    }
1903    validate_recovery_shape(recovery)
1904        .map_err(|_| store_failure(ExactWorkflowStoreError::Malformed, Some(recovery.clone())))?;
1905    if !recovery_matches_intent(recovery, intent) {
1906        return Err(store_failure(
1907            ExactWorkflowStoreError::IntentMismatch,
1908            Some(recovery.clone()),
1909        ));
1910    }
1911    Ok(())
1912}
1913
1914fn validate_recovery_descendant(
1915    current: &ExactWorkflowRecoveryV2,
1916    observed: &ExactWorkflowRecoveryV2,
1917) -> Result<(), ExactWorkflowError> {
1918    if observed.format_version != EXACT_MUTATION_FORMAT_VERSION {
1919        return Err(store_failure(
1920            ExactWorkflowStoreError::UnsupportedFormat {
1921                observed: observed.format_version,
1922            },
1923            Some(current.clone()),
1924        ));
1925    }
1926    validate_recovery_shape(observed)
1927        .map_err(|_| store_failure(ExactWorkflowStoreError::Malformed, Some(current.clone())))?;
1928    let clock_monotonic = current.last_observed_unix_millis.as_ref().is_none_or(|prior| {
1929        observed
1930            .last_observed_unix_millis
1931            .as_ref()
1932            .and_then(parse_u64)
1933            .zip(parse_u64(prior))
1934            .is_some_and(|(next, prior)| next >= prior)
1935    });
1936    let disposition_monotonic = current.submission_disposition.is_none_or(|prior| {
1937        observed
1938            .submission_disposition
1939            .is_some_and(|next| valid_disposition_successor(Some(prior), next))
1940    });
1941    if !same_recovery_intent(current, observed) {
1942        return Err(store_failure(
1943            ExactWorkflowStoreError::IntentMismatch,
1944            Some(current.clone()),
1945        ));
1946    }
1947    if observed.revision <= current.revision
1948        || observed.phase < current.phase
1949        || !clock_monotonic
1950        || current
1951            .preparation
1952            .as_ref()
1953            .is_some_and(|value| observed.preparation.as_ref() != Some(value))
1954        || !disposition_monotonic
1955        || current
1956            .terminal
1957            .as_ref()
1958            .is_some_and(|value| observed.terminal.as_ref() != Some(value))
1959    {
1960        return Err(store_failure(ExactWorkflowStoreError::Malformed, Some(current.clone())));
1961    }
1962    Ok(())
1963}
1964
1965fn transition_is_reflected(state: &ExactWorkflowRecoveryV2, transition: &ExactMutationTransition) -> bool {
1966    if state.terminal.is_some() {
1967        // INVARIANT: the first CAS-committed terminal result is authoritative.
1968        // A losing replica must adopt it instead of attempting to overwrite it
1969        // or returning a result derived from stale local observations.
1970        return true;
1971    }
1972    match transition {
1973        ExactMutationTransition::ClockObserved { unix_millis } => parse_u64(unix_millis).is_some_and(|requested| {
1974            state
1975                .last_observed_unix_millis
1976                .as_ref()
1977                .and_then(parse_u64)
1978                .is_some_and(|observed| observed >= requested)
1979        }),
1980        ExactMutationTransition::Prepared { preparation } => state.preparation.as_ref() == Some(preparation),
1981        ExactMutationTransition::SubmissionStarted => {
1982            state.preparation.is_some()
1983                && (state.phase == ExactWorkflowPhaseV2::SubmissionStarted
1984                    || state.phase >= ExactWorkflowPhaseV2::SubmissionObserved
1985                        && state.submission_disposition != Some(ExactSubmissionDispositionV1::NotSubmitted))
1986        }
1987        ExactMutationTransition::SubmissionObserved { disposition } => state
1988            .submission_disposition
1989            .is_some_and(|observed| valid_disposition_successor(Some(*disposition), observed)),
1990        ExactMutationTransition::Terminal { .. } => false,
1991    }
1992}
1993
1994fn same_recovery_intent(left: &ExactWorkflowRecoveryV2, right: &ExactWorkflowRecoveryV2) -> bool {
1995    left.format_version == right.format_version
1996        && left.mutation_kind == right.mutation_kind
1997        && left.request_sha256 == right.request_sha256
1998        && left.cluster_id == right.cluster_id
1999        && left.collection_id == right.collection_id
2000        && left.index == right.index
2001        && left.caller_action_id == right.caller_action_id
2002        && left.effect_deadline_unix_millis == right.effect_deadline_unix_millis
2003}
2004
2005fn recovery_matches_intent(recovery: &ExactWorkflowRecoveryV2, intent: &ExactMutationIntent) -> bool {
2006    recovery.format_version == intent.format_version
2007        && recovery.mutation_kind == intent.mutation_kind
2008        && recovery.request_sha256 == intent.request_sha256
2009        && recovery.cluster_id == intent.cluster_id
2010        && recovery.collection_id == intent.collection_id
2011        && recovery.index == intent.index
2012        && recovery.caller_action_id == intent.caller_action_id
2013}
2014
2015fn store_failure(source: ExactWorkflowStoreError, recovery: Option<ExactWorkflowRecoveryV2>) -> ExactWorkflowError {
2016    ExactWorkflowError::Store {
2017        source,
2018        last_durable_recovery: recovery.map(Box::new),
2019    }
2020}
2021
2022async fn record_resolution_observation(
2023    session: &ExactSession,
2024    recovery: &mut ExactWorkflowRecoveryV2,
2025    disposition: ExactSubmissionDispositionV1,
2026) -> Result<(), ExactWorkflowError> {
2027    // INVARIANT: a reflected disposition is as authoritative as one committed
2028    // by this caller. Only SubmissionStarted ownership controls whether this
2029    // caller may perform an effect; resolution and visibility remain reads.
2030    session
2031        .record(recovery, ExactMutationTransition::SubmissionObserved { disposition })
2032        .await
2033        .map(|_| ())
2034}
2035
2036async fn observe_clock(
2037    session: &ExactSession,
2038    recovery: &mut ExactWorkflowRecoveryV2,
2039    options: &ExactWorkflowOptions,
2040) -> Result<u64, ExactWorkflowError> {
2041    let now = options.clock.unix_millis()?;
2042    // INVARIANT: once submission may have happened, later wall-clock samples
2043    // cannot fence another effect. Only an authoritative not-submitted result
2044    // reopens submission, so visibility polling must not consume the bounded
2045    // durable journal with liveness-only clock observations.
2046    if recovery.phase >= ExactWorkflowPhaseV2::SubmissionStarted
2047        && recovery.submission_disposition != Some(ExactSubmissionDispositionV1::NotSubmitted)
2048    {
2049        return Ok(now);
2050    }
2051    if recovery
2052        .last_observed_unix_millis
2053        .as_ref()
2054        .and_then(parse_u64)
2055        .is_some_and(|prior| now <= prior)
2056    {
2057        return Ok(now);
2058    }
2059    session
2060        .record(
2061            recovery,
2062            ExactMutationTransition::ClockObserved {
2063                unix_millis: wire_u64(now),
2064            },
2065        )
2066        .await?;
2067    Ok(now)
2068}
2069
2070fn clock_regressed(recovery: &ExactWorkflowRecoveryV2, now: u64) -> bool {
2071    recovery
2072        .last_observed_unix_millis
2073        .as_ref()
2074        .and_then(parse_u64)
2075        .is_some_and(|observed| now < observed)
2076}
2077
2078fn recovery_effect_deadline(recovery: &ExactWorkflowRecoveryV2) -> Result<u64, ExactWorkflowError> {
2079    parse_u64(&recovery.effect_deadline_unix_millis)
2080        .filter(|value| *value > 0)
2081        .ok_or(ExactWorkflowError::InvalidRecovery)
2082}
2083
2084fn effect_options(now: u64, deadline: u64, options: &ExactWorkflowOptions) -> Result<CallOptions, ExactWorkflowError> {
2085    bounded_options(now, deadline, options)
2086}
2087
2088fn resolution_options(now: u64, options: &ExactWorkflowOptions) -> Result<Option<CallOptions>, ExactWorkflowError> {
2089    let deadline = if now < options.effect_deadline_unix_millis {
2090        options.effect_deadline_unix_millis
2091    } else {
2092        match options.observation_deadline_unix_millis {
2093            Some(value) if value > now => value,
2094            _ => return Ok(None),
2095        }
2096    };
2097    bounded_options(now, deadline, options).map(Some)
2098}
2099
2100fn bounded_options(now: u64, deadline: u64, options: &ExactWorkflowOptions) -> Result<CallOptions, ExactWorkflowError> {
2101    if deadline <= now {
2102        return Err(ExactWorkflowError::InvalidInput);
2103    }
2104    let bounded = deadline.min(now.saturating_add(MAX_EFFECT_CALL_MILLIS));
2105    CallOptions::from_absolute_deadline_unix_millis(bounded)
2106        .map(|value| value.with_cancellation(options.cancellation.clone()))
2107        .map_err(ExactWorkflowError::Client)
2108}
2109
2110fn validate_append_request(request: &AppendExactRequestV1) -> Result<(), ExactWorkflowError> {
2111    if request.batch_id.validate().is_err()
2112        || !valid_index(&request.index)
2113        || request.items.is_empty()
2114        || request.items.len() > 1_000
2115        || request.items.iter().any(|item| {
2116            item.item_action_id.validate().is_err() || item.document_id.is_empty() || item.document_id.len() > 512
2117        })
2118    {
2119        return Err(ExactWorkflowError::InvalidInput);
2120    }
2121    let mut identities = std::collections::BTreeSet::new();
2122    if request
2123        .items
2124        .iter()
2125        .any(|item| !identities.insert(item.item_action_id.value.as_str()))
2126    {
2127        return Err(ExactWorkflowError::InvalidInput);
2128    }
2129    Ok(())
2130}
2131
2132fn validate_append_outcome(
2133    request: &AppendExactRequestV1,
2134    outcome: &RetainedAppendBulkOutcome,
2135    prepared: bool,
2136) -> Result<(), ExactWorkflowError> {
2137    if outcome.batch_id != request.batch_id
2138        || outcome.index != request.index
2139        || outcome.items.len() != request.items.len()
2140    {
2141        return Err(ExactWorkflowError::CoreShape);
2142    }
2143    for (expected, observed) in request.items.iter().zip(&outcome.items) {
2144        if observed.item_action_id != expected.item_action_id
2145            || observed
2146                .locator
2147                .as_ref()
2148                .is_some_and(|value| value.authority.is_empty() || value.value.is_empty())
2149            || observed
2150                .semantic_digest
2151                .as_ref()
2152                .is_some_and(|value| value.validate().is_err())
2153            || prepared
2154                && (observed.state != RetainedAppendState::Prepared
2155                    || observed.retry_safety != RetainedAppendRetrySafety::Safe
2156                    || observed.locator.is_none()
2157                    || observed.semantic_digest.is_none()
2158                    || observed.sequence.is_some()
2159                    || observed.primary_term.is_some()
2160                    || observed.error.is_some())
2161        {
2162            return Err(ExactWorkflowError::CoreShape);
2163        }
2164    }
2165    Ok(())
2166}
2167
2168fn redact_append_outcome(mut outcome: RetainedAppendBulkOutcome) -> RetainedAppendBulkOutcome {
2169    for item in &mut outcome.items {
2170        item.error = None;
2171    }
2172    outcome
2173}
2174
2175fn append_preparation(recovery: &ExactWorkflowRecoveryV2) -> Result<&RetainedAppendBulkOutcome, ExactWorkflowError> {
2176    match &recovery.preparation {
2177        Some(ExactPreparationV1::Append(value)) => Ok(value),
2178        _ => Err(ExactWorkflowError::InvalidRecovery),
2179    }
2180}
2181
2182fn document_preparation(recovery: &ExactWorkflowRecoveryV2) -> Result<(Locator, Digest), ExactWorkflowError> {
2183    match &recovery.preparation {
2184        Some(ExactPreparationV1::Document {
2185            locator,
2186            semantic_digest,
2187        }) => Ok((locator.clone(), semantic_digest.clone())),
2188        _ => Err(ExactWorkflowError::InvalidRecovery),
2189    }
2190}
2191
2192fn index_preparation(
2193    recovery: &ExactWorkflowRecoveryV2,
2194) -> Result<(Locator, U64, Digest, IndexResourceSpec), ExactWorkflowError> {
2195    match &recovery.preparation {
2196        Some(ExactPreparationV1::Index {
2197            locator,
2198            expires_at,
2199            semantic_digest,
2200            resource_spec,
2201        }) => Ok((
2202            locator.clone(),
2203            expires_at.clone(),
2204            semantic_digest.clone(),
2205            resource_spec.as_ref().clone(),
2206        )),
2207        _ => Err(ExactWorkflowError::InvalidRecovery),
2208    }
2209}
2210
2211fn append_terminal_result(
2212    request: &AppendExactRequestV1,
2213    outcome: &RetainedAppendBulkOutcome,
2214    recovered: bool,
2215) -> Option<AppendExactResultV1> {
2216    let state = if outcome
2217        .items
2218        .iter()
2219        .any(|item| item.state == RetainedAppendState::SubmittedIndeterminate)
2220    {
2221        return None;
2222    } else if outcome
2223        .items
2224        .iter()
2225        .any(|item| item.state == RetainedAppendState::Conflict)
2226    {
2227        ExactMutationOutcomeV1::Conflict
2228    } else if outcome
2229        .items
2230        .iter()
2231        .any(|item| item.state == RetainedAppendState::Retired)
2232    {
2233        ExactMutationOutcomeV1::KnownRetired
2234    } else if outcome.items.iter().any(|item| {
2235        matches!(
2236            item.state,
2237            RetainedAppendState::NotSubmittedRetryable | RetainedAppendState::NotSubmittedTerminal
2238        )
2239    }) {
2240        ExactMutationOutcomeV1::NotSubmitted
2241    } else if outcome.items.iter().all(|item| {
2242        matches!(
2243            item.state,
2244            RetainedAppendState::Committed | RetainedAppendState::Replayed
2245        )
2246    }) {
2247        if recovered {
2248            ExactMutationOutcomeV1::Resolved
2249        } else {
2250            ExactMutationOutcomeV1::Applied
2251        }
2252    } else {
2253        return None;
2254    };
2255    Some(append_result(request, outcome, state))
2256}
2257
2258fn append_result(
2259    request: &AppendExactRequestV1,
2260    outcome: &RetainedAppendBulkOutcome,
2261    state: ExactMutationOutcomeV1,
2262) -> AppendExactResultV1 {
2263    AppendExactResultV1 {
2264        outcome: state,
2265        batch_id: request.batch_id.clone(),
2266        index: request.index.clone(),
2267        items: outcome.items.iter().map(redact_append_item).collect(),
2268        visibility: Vec::new(),
2269    }
2270}
2271
2272fn append_status(
2273    request: &AppendExactRequestV1,
2274    recovery: &ExactWorkflowRecoveryV2,
2275    state: ExactMutationOutcomeV1,
2276) -> AppendExactResultV1 {
2277    let items = append_preparation(recovery)
2278        .map(|value| value.items.iter().map(redact_append_item).collect())
2279        .unwrap_or_default();
2280    AppendExactResultV1 {
2281        outcome: state,
2282        batch_id: request.batch_id.clone(),
2283        index: request.index.clone(),
2284        items,
2285        visibility: Vec::new(),
2286    }
2287}
2288
2289fn redact_append_item(item: &RetainedAppendItemOutcome) -> AppendExactItemResultV1 {
2290    AppendExactItemResultV1 {
2291        item_action_id: item.item_action_id.clone(),
2292        locator: item.locator.clone(),
2293        semantic_digest: item.semantic_digest.clone(),
2294        state: item.state,
2295        retry_safety: item.retry_safety,
2296        sequence: item.sequence.clone(),
2297        primary_term: item.primary_term.clone(),
2298    }
2299}
2300
2301fn document_status(
2302    request: &PrepareDocumentMutationRequest,
2303    recovery: &ExactWorkflowRecoveryV2,
2304    outcome: ExactMutationOutcomeV1,
2305) -> DocumentMutationExactResultV1 {
2306    let locator = document_preparation(recovery).map(|value| value.0).ok();
2307    DocumentMutationExactResultV1 {
2308        outcome,
2309        index: request.index.clone(),
2310        locator,
2311        state: outcome_name(outcome).to_owned(),
2312        submitted: recovery
2313            .submission_disposition
2314            .is_some_and(|value| !matches!(value, ExactSubmissionDispositionV1::NotSubmitted)),
2315        sequence: None,
2316        mapping_generation: None,
2317        mapping_adopted: false,
2318    }
2319}
2320
2321fn index_status(
2322    request: &PrepareCreateIndexRequest,
2323    recovery: &ExactWorkflowRecoveryV2,
2324    outcome: ExactMutationOutcomeV1,
2325) -> ReconcileIndexExactResultV1 {
2326    let locator = match &recovery.preparation {
2327        Some(ExactPreparationV1::Index { locator, .. }) => Some(locator.clone()),
2328        _ => None,
2329    };
2330    ReconcileIndexExactResultV1 {
2331        outcome,
2332        name: request.name.clone(),
2333        locator,
2334        resource: None,
2335        state: outcome_name(outcome).to_owned(),
2336    }
2337}
2338
2339const fn outcome_name(outcome: ExactMutationOutcomeV1) -> &'static str {
2340    match outcome {
2341        ExactMutationOutcomeV1::AlreadySatisfied => "already_satisfied",
2342        ExactMutationOutcomeV1::Applied => "applied",
2343        ExactMutationOutcomeV1::Resolved => "resolved",
2344        ExactMutationOutcomeV1::Conflict => "conflict",
2345        ExactMutationOutcomeV1::NotSubmitted => "not_submitted",
2346        ExactMutationOutcomeV1::Indeterminate => "indeterminate",
2347        ExactMutationOutcomeV1::KnownRetired => "known_retired",
2348        ExactMutationOutcomeV1::EffectDeadlineExpired => "effect_deadline_expired",
2349        ExactMutationOutcomeV1::ClockRegression => "clock_regression",
2350    }
2351}
2352
2353async fn finish_append(
2354    session: &ExactSession,
2355    recovery: &mut ExactWorkflowRecoveryV2,
2356    result: AppendExactResultV1,
2357) -> Result<ExactExecutionV2<AppendExactResultV1>, ExactWorkflowError> {
2358    session
2359        .record(
2360            recovery,
2361            ExactMutationTransition::Terminal {
2362                result: ExactTerminalResultV1::Append(result.clone()),
2363            },
2364        )
2365        .await?;
2366    let durable = match &recovery.terminal {
2367        Some(ExactTerminalResultV1::Append(durable)) => durable.clone(),
2368        _ => return Err(ExactWorkflowError::InvalidRecovery),
2369    };
2370    Ok(execution(durable, recovery.clone()))
2371}
2372
2373async fn finish_document(
2374    session: &ExactSession,
2375    recovery: &mut ExactWorkflowRecoveryV2,
2376    result: DocumentMutationExactResultV1,
2377) -> Result<ExactExecutionV2<DocumentMutationExactResultV1>, ExactWorkflowError> {
2378    session
2379        .record(
2380            recovery,
2381            ExactMutationTransition::Terminal {
2382                result: ExactTerminalResultV1::Document(result.clone()),
2383            },
2384        )
2385        .await?;
2386    let durable = match &recovery.terminal {
2387        Some(ExactTerminalResultV1::Document(durable)) => durable.clone(),
2388        _ => return Err(ExactWorkflowError::InvalidRecovery),
2389    };
2390    Ok(execution(durable, recovery.clone()))
2391}
2392
2393async fn finish_index(
2394    session: &ExactSession,
2395    recovery: &mut ExactWorkflowRecoveryV2,
2396    result: ReconcileIndexExactResultV1,
2397) -> Result<ExactExecutionV2<ReconcileIndexExactResultV1>, ExactWorkflowError> {
2398    session
2399        .record(
2400            recovery,
2401            ExactMutationTransition::Terminal {
2402                result: ExactTerminalResultV1::Index(result.clone()),
2403            },
2404        )
2405        .await?;
2406    let durable = match &recovery.terminal {
2407        Some(ExactTerminalResultV1::Index(durable)) => durable.clone(),
2408        _ => return Err(ExactWorkflowError::InvalidRecovery),
2409    };
2410    Ok(execution(durable, recovery.clone()))
2411}
2412
2413fn execution<T>(result: T, recovery: ExactWorkflowRecoveryV2) -> ExactExecutionV2<T> {
2414    ExactExecutionV2 { result, recovery }
2415}
2416
2417fn valid_preparation(value: &ExactPreparationV1) -> bool {
2418    match value {
2419        ExactPreparationV1::Append(outcome) => {
2420            !outcome.items.is_empty()
2421                && outcome.items.len() <= 1_000
2422                && outcome.items.iter().all(|item| {
2423                    item.item_action_id.validate().is_ok()
2424                        && item
2425                            .locator
2426                            .as_ref()
2427                            .is_some_and(|locator| !locator.authority.is_empty() && !locator.value.is_empty())
2428                        && item
2429                            .semantic_digest
2430                            .as_ref()
2431                            .is_some_and(|digest| digest.validate().is_ok())
2432                        && item.state == RetainedAppendState::Prepared
2433                        && item.retry_safety == RetainedAppendRetrySafety::Safe
2434                        && item.error.is_none()
2435                })
2436        }
2437        ExactPreparationV1::Document {
2438            locator,
2439            semantic_digest,
2440        } => !locator.authority.is_empty() && !locator.value.is_empty() && semantic_digest.validate().is_ok(),
2441        ExactPreparationV1::Index {
2442            locator,
2443            expires_at,
2444            semantic_digest,
2445            ..
2446        } => {
2447            !locator.authority.is_empty()
2448                && !locator.value.is_empty()
2449                && parse_u64(expires_at).is_some_and(|value| value > 0)
2450                && semantic_digest.validate().is_ok()
2451        }
2452    }
2453}
2454
2455fn valid_terminal(value: &ExactTerminalResultV1) -> bool {
2456    match value {
2457        ExactTerminalResultV1::Append(result) => {
2458            result.batch_id.validate().is_ok()
2459                && valid_index(&result.index)
2460                && terminal_outcome(result.outcome)
2461                && result.items.iter().all(|item| item.item_action_id.validate().is_ok())
2462        }
2463        ExactTerminalResultV1::Document(result) => {
2464            valid_index(&result.index)
2465                && result
2466                    .locator
2467                    .as_ref()
2468                    .is_none_or(|locator| !locator.authority.is_empty() && !locator.value.is_empty())
2469                && terminal_outcome(result.outcome)
2470        }
2471        ExactTerminalResultV1::Index(result) => !result.name.is_empty() && terminal_outcome(result.outcome),
2472    }
2473}
2474
2475const fn terminal_outcome(value: ExactMutationOutcomeV1) -> bool {
2476    matches!(
2477        value,
2478        ExactMutationOutcomeV1::AlreadySatisfied
2479            | ExactMutationOutcomeV1::Applied
2480            | ExactMutationOutcomeV1::Resolved
2481            | ExactMutationOutcomeV1::Conflict
2482            | ExactMutationOutcomeV1::KnownRetired
2483            | ExactMutationOutcomeV1::EffectDeadlineExpired
2484    )
2485}
2486
2487const fn preparation_kind(value: &ExactPreparationV1) -> ExactMutationKindV1 {
2488    match value {
2489        ExactPreparationV1::Append(_) => ExactMutationKindV1::Append,
2490        ExactPreparationV1::Document { .. } => ExactMutationKindV1::Document,
2491        ExactPreparationV1::Index { .. } => ExactMutationKindV1::Index,
2492    }
2493}
2494
2495const fn terminal_kind(value: &ExactTerminalResultV1) -> ExactMutationKindV1 {
2496    match value {
2497        ExactTerminalResultV1::Append(_) => ExactMutationKindV1::Append,
2498        ExactTerminalResultV1::Document(_) => ExactMutationKindV1::Document,
2499        ExactTerminalResultV1::Index(_) => ExactMutationKindV1::Index,
2500    }
2501}
2502
2503fn valid_index(value: &Resource) -> bool {
2504    value.kind == "index" && !value.name.is_empty() && value.name.len() <= 255 && value.incarnation.validate().is_ok()
2505}
2506
2507fn canonical_sha256<T: Serialize>(value: &T) -> Result<Digest, ExactWorkflowError> {
2508    let bytes = serde_json::to_vec(value).map_err(|_| ExactWorkflowError::RequestCodec)?;
2509    let observed = digest::digest(&digest::SHA256, &bytes);
2510    Ok(Digest {
2511        value: lower_hex(observed.as_ref()),
2512    })
2513}
2514
2515fn parse_u64(value: &U64) -> Option<u64> {
2516    if value.value.is_empty() || value.value.len() > 20 || value.value.starts_with('0') && value.value != "0" {
2517        return None;
2518    }
2519    value.value.parse().ok()
2520}
2521
2522fn wire_u64(value: u64) -> U64 {
2523    U64 {
2524        value: value.to_string(),
2525    }
2526}
2527
2528fn lower_hex(bytes: &[u8]) -> String {
2529    const HEX: &[u8; 16] = b"0123456789abcdef";
2530    let mut output = String::with_capacity(bytes.len().saturating_mul(2));
2531    for byte in bytes {
2532        output.push(char::from(HEX[usize::from(byte >> 4)]));
2533        output.push(char::from(HEX[usize::from(byte & 0x0f)]));
2534    }
2535    output
2536}
2537
2538#[cfg(test)]
2539mod tests {
2540    use std::os::unix::fs::PermissionsExt as _;
2541    use std::sync::Mutex;
2542
2543    use super::*;
2544
2545    #[derive(Default)]
2546    struct PostgresShapedRecoveryStore {
2547        row: Mutex<Option<ExactWorkflowRecoveryV2>>,
2548    }
2549
2550    impl ExactWorkflowRecoveryStore for PostgresShapedRecoveryStore {
2551        fn load(
2552            &self,
2553        ) -> ExactWorkflowStoreFuture<'_, Result<Option<ExactWorkflowRecoveryV2>, ExactWorkflowStoreError>> {
2554            Box::pin(async move {
2555                self.row
2556                    .lock()
2557                    .map(|row| row.clone())
2558                    .map_err(|_| ExactWorkflowStoreError::Internal)
2559            })
2560        }
2561
2562        fn compare_exchange(
2563            &self,
2564            expected_revision: Option<u64>,
2565            next: ExactWorkflowRecoveryV2,
2566        ) -> ExactWorkflowStoreFuture<'_, Result<(), ExactWorkflowStoreError>> {
2567            Box::pin(async move {
2568                let mut row = self.row.lock().map_err(|_| ExactWorkflowStoreError::Internal)?;
2569                let observed_revision = row.as_ref().map(|value| value.revision);
2570                if observed_revision != expected_revision {
2571                    return Err(ExactWorkflowStoreError::Conflict {
2572                        observed: row.clone().map(Box::new),
2573                    });
2574                }
2575                *row = Some(next);
2576                Ok(())
2577            })
2578        }
2579    }
2580
2581    struct AdvancingConflictStore {
2582        row: Mutex<ExactWorkflowRecoveryV2>,
2583    }
2584
2585    struct UnavailableRecoveryStore;
2586
2587    struct AdvancingClock {
2588        next: Mutex<u64>,
2589    }
2590
2591    impl ExactWorkflowClock for AdvancingClock {
2592        fn unix_millis(&self) -> Result<u64, ExactWorkflowError> {
2593            let mut next = self.next.lock().map_err(|_| ExactWorkflowError::ClockUnavailable)?;
2594            let observed = *next;
2595            *next = next.saturating_add(1);
2596            Ok(observed)
2597        }
2598    }
2599
2600    impl ExactWorkflowRecoveryStore for UnavailableRecoveryStore {
2601        fn load(
2602            &self,
2603        ) -> ExactWorkflowStoreFuture<'_, Result<Option<ExactWorkflowRecoveryV2>, ExactWorkflowStoreError>> {
2604            Box::pin(async { Err(ExactWorkflowStoreError::Unavailable) })
2605        }
2606
2607        fn compare_exchange(
2608            &self,
2609            _expected_revision: Option<u64>,
2610            _next: ExactWorkflowRecoveryV2,
2611        ) -> ExactWorkflowStoreFuture<'_, Result<(), ExactWorkflowStoreError>> {
2612            Box::pin(async { Err(ExactWorkflowStoreError::Unavailable) })
2613        }
2614    }
2615
2616    impl ExactWorkflowRecoveryStore for AdvancingConflictStore {
2617        fn load(
2618            &self,
2619        ) -> ExactWorkflowStoreFuture<'_, Result<Option<ExactWorkflowRecoveryV2>, ExactWorkflowStoreError>> {
2620            Box::pin(async move {
2621                self.row
2622                    .lock()
2623                    .map(|row| Some(row.clone()))
2624                    .map_err(|_| ExactWorkflowStoreError::Internal)
2625            })
2626        }
2627
2628        fn compare_exchange(
2629            &self,
2630            _expected_revision: Option<u64>,
2631            _next: ExactWorkflowRecoveryV2,
2632        ) -> ExactWorkflowStoreFuture<'_, Result<(), ExactWorkflowStoreError>> {
2633            Box::pin(async move {
2634                let mut row = self.row.lock().map_err(|_| ExactWorkflowStoreError::Internal)?;
2635                let next_clock = row
2636                    .last_observed_unix_millis
2637                    .as_ref()
2638                    .and_then(parse_u64)
2639                    .unwrap_or(0)
2640                    .saturating_add(1);
2641                reduce_transition(
2642                    &mut row,
2643                    &ExactMutationTransition::ClockObserved {
2644                        unix_millis: wire_u64(next_clock),
2645                    },
2646                )
2647                .map_err(|_| ExactWorkflowStoreError::Internal)?;
2648                Err(ExactWorkflowStoreError::Conflict {
2649                    observed: Some(Box::new(row.clone())),
2650                })
2651            })
2652        }
2653    }
2654
2655    fn identity(character: char) -> Identity {
2656        Identity {
2657            value: character.to_string().repeat(32),
2658        }
2659    }
2660
2661    fn digest(character: char) -> Digest {
2662        Digest {
2663            value: character.to_string().repeat(64),
2664        }
2665    }
2666
2667    fn resource() -> Resource {
2668        Resource {
2669            kind: "index".to_owned(),
2670            name: "events".to_owned(),
2671            incarnation: identity('a'),
2672        }
2673    }
2674
2675    fn append_preparation_value() -> ExactPreparationV1 {
2676        ExactPreparationV1::Append(RetainedAppendBulkOutcome {
2677            batch_id: digest('e'),
2678            index: resource(),
2679            items: vec![RetainedAppendItemOutcome {
2680                item_action_id: digest('f'),
2681                locator: Some(Locator {
2682                    authority: "retained_append".to_owned(),
2683                    value: "01".to_owned(),
2684                }),
2685                semantic_digest: Some(digest('1')),
2686                state: RetainedAppendState::Prepared,
2687                retry_safety: RetainedAppendRetrySafety::Safe,
2688                sequence: None,
2689                primary_term: None,
2690                error: None,
2691            }],
2692            complete: true,
2693            achievement: None,
2694        })
2695    }
2696
2697    #[test]
2698    fn retained_event_identity_preserves_the_catalog_submit_kind() {
2699        assert_eq!(create_index_action_kind(None), "create_index");
2700        assert_eq!(create_index_action_kind(Some(false)), "create_index");
2701        assert_eq!(create_index_action_kind(Some(true)), "create_retained_append_log_index");
2702    }
2703
2704    #[test]
2705    fn catalog_recovery_replays_only_an_exact_prepared_or_pending_identity() {
2706        assert!(catalog_resolution_requires_exact_replay("pending", None));
2707        assert!(catalog_resolution_requires_exact_replay(
2708            "prepared",
2709            Some(ExactSubmissionDispositionV1::Indeterminate),
2710        ));
2711        assert!(catalog_resolution_requires_exact_replay(
2712            "prepared",
2713            Some(ExactSubmissionDispositionV1::Submitted),
2714        ));
2715        assert!(!catalog_resolution_requires_exact_replay("prepared", None));
2716        assert!(!catalog_resolution_requires_exact_replay(
2717            "prepared",
2718            Some(ExactSubmissionDispositionV1::NotSubmitted),
2719        ));
2720        assert!(!catalog_resolution_requires_exact_replay(
2721            "expired_unknown",
2722            Some(ExactSubmissionDispositionV1::Indeterminate),
2723        ));
2724    }
2725
2726    #[test]
2727    fn recovery_refuses_changed_request_or_scope_and_retains_first_deadline() -> Result<(), Box<dyn Error>> {
2728        let intent = exact_intent(
2729            ExactMutationKindV1::Append,
2730            digest('b'),
2731            identity('c'),
2732            identity('d'),
2733            Some(resource()),
2734            digest('e'),
2735            100,
2736        );
2737        let recovery = recovery_from_intent(&intent);
2738        validate_recovery_matches(&recovery, &intent)?;
2739        assert_eq!(recovery_effect_deadline(&recovery)?, 100);
2740        let mut changed = intent.clone();
2741        changed.collection_id = identity('f');
2742        assert!(matches!(
2743            validate_recovery_matches(&recovery, &changed),
2744            Err(ExactWorkflowError::InvalidRecovery)
2745        ));
2746        changed = intent.clone();
2747        changed.request_sha256 = digest('f');
2748        assert!(matches!(
2749            validate_recovery_matches(&recovery, &changed),
2750            Err(ExactWorkflowError::InvalidRecovery)
2751        ));
2752        Ok(())
2753    }
2754
2755    #[test]
2756    fn reducer_never_regresses_clock_or_replaces_possible_submission() -> Result<(), Box<dyn Error>> {
2757        let intent = exact_intent(
2758            ExactMutationKindV1::Append,
2759            digest('b'),
2760            identity('c'),
2761            identity('d'),
2762            Some(resource()),
2763            digest('e'),
2764            100,
2765        );
2766        let mut state = recovery_from_intent(&intent);
2767        reduce_transition(
2768            &mut state,
2769            &ExactMutationTransition::ClockObserved {
2770                unix_millis: wire_u64(10),
2771            },
2772        )?;
2773        assert!(
2774            reduce_transition(
2775                &mut state,
2776                &ExactMutationTransition::ClockObserved {
2777                    unix_millis: wire_u64(9),
2778                },
2779            )
2780            .is_err()
2781        );
2782        let preparation = ExactPreparationV1::Append(RetainedAppendBulkOutcome {
2783            batch_id: digest('e'),
2784            index: resource(),
2785            items: vec![RetainedAppendItemOutcome {
2786                item_action_id: digest('f'),
2787                locator: Some(Locator {
2788                    authority: "retained_append".to_owned(),
2789                    value: "01".to_owned(),
2790                }),
2791                semantic_digest: Some(digest('1')),
2792                state: RetainedAppendState::Prepared,
2793                retry_safety: RetainedAppendRetrySafety::Safe,
2794                sequence: None,
2795                primary_term: None,
2796                error: None,
2797            }],
2798            complete: true,
2799            achievement: None,
2800        });
2801        reduce_transition(
2802            &mut state,
2803            &ExactMutationTransition::Prepared {
2804                preparation: preparation.clone(),
2805            },
2806        )?;
2807        reduce_transition(&mut state, &ExactMutationTransition::SubmissionStarted)?;
2808        reduce_transition(
2809            &mut state,
2810            &ExactMutationTransition::SubmissionObserved {
2811                disposition: ExactSubmissionDispositionV1::Indeterminate,
2812            },
2813        )?;
2814        assert!(reduce_transition(&mut state, &ExactMutationTransition::Prepared { preparation },).is_err());
2815        Ok(())
2816    }
2817
2818    #[test]
2819    fn reducer_refuses_revision_overflow_without_changing_state() {
2820        let intent = exact_intent(
2821            ExactMutationKindV1::Append,
2822            digest('b'),
2823            identity('c'),
2824            identity('d'),
2825            Some(resource()),
2826            digest('e'),
2827            100,
2828        );
2829        let mut state = recovery_from_intent(&intent);
2830        state.revision = MAX_SAFE_REVISION;
2831        let unchanged = state.clone();
2832        assert!(
2833            reduce_transition(
2834                &mut state,
2835                &ExactMutationTransition::ClockObserved {
2836                    unix_millis: wire_u64(1),
2837                },
2838            )
2839            .is_err()
2840        );
2841        assert_eq!(state, unchanged);
2842    }
2843
2844    #[test]
2845    fn not_submitted_append_outcome_remains_recovery_resumable() -> Result<(), Box<dyn Error>> {
2846        let request = AppendExactRequestV1 {
2847            batch_id: digest('a'),
2848            index: resource(),
2849            items: vec![RetainedAppendItem {
2850                item_action_id: digest('b'),
2851                document_id: "event-1".to_owned(),
2852                record: lodestone_wire::NativeRecord::Document {
2853                    source: lodestone_wire::RawJson {
2854                        bytes_base64: "e30=".to_owned(),
2855                    },
2856                },
2857            }],
2858            observe_committed: true,
2859        };
2860        let outcome = RetainedAppendBulkOutcome {
2861            batch_id: request.batch_id.clone(),
2862            index: request.index.clone(),
2863            items: vec![RetainedAppendItemOutcome {
2864                item_action_id: request.items[0].item_action_id.clone(),
2865                locator: Some(Locator {
2866                    authority: "retained_append".to_owned(),
2867                    value: "01".to_owned(),
2868                }),
2869                semantic_digest: Some(digest('c')),
2870                state: RetainedAppendState::NotSubmittedRetryable,
2871                retry_safety: RetainedAppendRetrySafety::Safe,
2872                sequence: None,
2873                primary_term: None,
2874                error: None,
2875            }],
2876            complete: false,
2877            achievement: None,
2878        };
2879
2880        let result = append_terminal_result(&request, &outcome, false).ok_or("not-submitted observation absent")?;
2881        assert_eq!(result.outcome, ExactMutationOutcomeV1::NotSubmitted);
2882        assert!(!terminal_outcome(result.outcome));
2883        Ok(())
2884    }
2885
2886    #[test]
2887    fn append_recovery_redacts_server_error_messages() {
2888        let outcome = RetainedAppendBulkOutcome {
2889            batch_id: digest('a'),
2890            index: resource(),
2891            items: vec![RetainedAppendItemOutcome {
2892                item_action_id: digest('b'),
2893                locator: Some(Locator {
2894                    authority: "retained_append".to_owned(),
2895                    value: "01".to_owned(),
2896                }),
2897                semantic_digest: Some(digest('c')),
2898                state: RetainedAppendState::Prepared,
2899                retry_safety: RetainedAppendRetrySafety::Safe,
2900                sequence: None,
2901                primary_term: None,
2902                error: None,
2903            }],
2904            complete: true,
2905            achievement: None,
2906        };
2907        assert!(redact_append_outcome(outcome).items[0].error.is_none());
2908    }
2909
2910    #[tokio::test]
2911    async fn caller_owned_cas_converges_two_replicas_across_every_phase() -> Result<(), Box<dyn Error>> {
2912        let store: Arc<dyn ExactWorkflowRecoveryStore> = Arc::new(PostgresShapedRecoveryStore::default());
2913        let intent = exact_intent(
2914            ExactMutationKindV1::Append,
2915            digest('b'),
2916            identity('c'),
2917            identity('d'),
2918            Some(resource()),
2919            digest('e'),
2920            10_000,
2921        );
2922        let options = ExactWorkflowOptions::new(10_000)
2923            .with_observation_deadline_unix_millis(20_000)
2924            .with_recovery_store(Arc::clone(&store));
2925        let (left_session, mut left) = open_session(&intent, &options).await?;
2926        let (right_session, mut right) = open_session(&intent, &options).await?;
2927        assert_eq!(left.revision, 0);
2928        assert_eq!(right.revision, 0);
2929
2930        assert!(
2931            left_session
2932                .record(
2933                    &mut left,
2934                    ExactMutationTransition::ClockObserved {
2935                        unix_millis: wire_u64(1),
2936                    },
2937                )
2938                .await?
2939        );
2940        assert!(
2941            !right_session
2942                .record(
2943                    &mut right,
2944                    ExactMutationTransition::ClockObserved {
2945                        unix_millis: wire_u64(1),
2946                    },
2947                )
2948                .await?
2949        );
2950
2951        let preparation = append_preparation_value();
2952        assert!(
2953            left_session
2954                .record(
2955                    &mut left,
2956                    ExactMutationTransition::Prepared {
2957                        preparation: preparation.clone(),
2958                    },
2959                )
2960                .await?
2961        );
2962        assert!(
2963            !right_session
2964                .record(&mut right, ExactMutationTransition::Prepared { preparation },)
2965                .await?
2966        );
2967
2968        assert!(
2969            left_session
2970                .record(&mut left, ExactMutationTransition::SubmissionStarted)
2971                .await?
2972        );
2973        assert!(
2974            !right_session
2975                .record(&mut right, ExactMutationTransition::SubmissionStarted)
2976                .await?
2977        );
2978
2979        let observation = ExactMutationTransition::SubmissionObserved {
2980            disposition: ExactSubmissionDispositionV1::Resolved,
2981        };
2982        assert!(left_session.record(&mut left, observation.clone()).await?);
2983        assert!(!right_session.record(&mut right, observation).await?);
2984
2985        let result = ExactTerminalResultV1::Append(AppendExactResultV1 {
2986            outcome: ExactMutationOutcomeV1::Resolved,
2987            batch_id: digest('e'),
2988            index: resource(),
2989            items: Vec::new(),
2990            visibility: Vec::new(),
2991        });
2992        assert!(
2993            left_session
2994                .record(&mut left, ExactMutationTransition::Terminal { result: result.clone() },)
2995                .await?
2996        );
2997        let losing = AppendExactResultV1 {
2998            outcome: ExactMutationOutcomeV1::Conflict,
2999            batch_id: digest('e'),
3000            index: resource(),
3001            items: Vec::new(),
3002            visibility: Vec::new(),
3003        };
3004        let adopted = finish_append(&right_session, &mut right, losing).await?;
3005        assert_eq!(adopted.result.outcome, ExactMutationOutcomeV1::Resolved);
3006        assert_eq!(left, right);
3007        assert_eq!(left.phase, ExactWorkflowPhaseV2::Terminal);
3008        assert_eq!(left.revision, 5);
3009        assert_eq!(store.load().await?, Some(left));
3010        Ok(())
3011    }
3012
3013    #[test]
3014    fn rust_recovery_v2_matches_the_cross_language_fixture() -> Result<(), Box<dyn Error>> {
3015        let fixture: serde_json::Value = serde_json::from_str(include_str!(
3016            "../../../clients/native/fixtures/lodestone-exact-workflow-recovery-v2.json"
3017        ))?;
3018        if fixture.get("format_version").and_then(serde_json::Value::as_u64) != Some(2) {
3019            return Err("exact recovery fixture format differs".into());
3020        }
3021        let values = fixture
3022            .get("states")
3023            .and_then(serde_json::Value::as_array)
3024            .ok_or("exact recovery fixture states are absent")?;
3025        let states: Vec<ExactWorkflowRecoveryV2> = values
3026            .iter()
3027            .cloned()
3028            .map(serde_json::from_value)
3029            .collect::<Result<_, _>>()?;
3030        assert_eq!(states.len(), 6);
3031        for state in &states {
3032            validate_recovery_shape(state)?;
3033        }
3034        for pair in states.windows(2) {
3035            let [current, next] = pair else {
3036                return Err("exact recovery fixture window differs".into());
3037            };
3038            let transition = transition_between(current, next)?;
3039            let mut reduced = current.clone();
3040            reduce_transition(&mut reduced, &transition)?;
3041            assert_eq!(&reduced, next);
3042        }
3043        assert_eq!(
3044            states.last().map(|value| value.phase),
3045            Some(ExactWorkflowPhaseV2::Terminal)
3046        );
3047        Ok(())
3048    }
3049
3050    #[tokio::test]
3051    async fn store_values_fail_closed_by_format_shape_and_intent() -> Result<(), Box<dyn Error>> {
3052        let intent = exact_intent(
3053            ExactMutationKindV1::Append,
3054            digest('b'),
3055            identity('c'),
3056            identity('d'),
3057            Some(resource()),
3058            digest('e'),
3059            10_000,
3060        );
3061        let initial = recovery_from_intent(&intent);
3062
3063        let mut future = initial.clone();
3064        future.format_version = EXACT_MUTATION_FORMAT_VERSION.saturating_add(1);
3065        let future_store: Arc<dyn ExactWorkflowRecoveryStore> = Arc::new(PostgresShapedRecoveryStore {
3066            row: Mutex::new(Some(future)),
3067        });
3068        let future_result = open_session(
3069            &intent,
3070            &ExactWorkflowOptions::new(10_000).with_recovery_store(future_store),
3071        )
3072        .await;
3073        assert!(matches!(
3074            future_result,
3075            Err(ExactWorkflowError::Store {
3076                source: ExactWorkflowStoreError::UnsupportedFormat { .. },
3077                ..
3078            })
3079        ));
3080
3081        let mut malformed = initial.clone();
3082        malformed.phase = ExactWorkflowPhaseV2::Prepared;
3083        let malformed_store: Arc<dyn ExactWorkflowRecoveryStore> = Arc::new(PostgresShapedRecoveryStore {
3084            row: Mutex::new(Some(malformed)),
3085        });
3086        let malformed_result = open_session(
3087            &intent,
3088            &ExactWorkflowOptions::new(10_000).with_recovery_store(malformed_store),
3089        )
3090        .await;
3091        assert!(matches!(
3092            malformed_result,
3093            Err(ExactWorkflowError::Store {
3094                source: ExactWorkflowStoreError::Malformed,
3095                ..
3096            })
3097        ));
3098
3099        let mut mismatched = initial;
3100        mismatched.request_sha256 = digest('9');
3101        let mismatch_store: Arc<dyn ExactWorkflowRecoveryStore> = Arc::new(PostgresShapedRecoveryStore {
3102            row: Mutex::new(Some(mismatched)),
3103        });
3104        let mismatch_result = open_session(
3105            &intent,
3106            &ExactWorkflowOptions::new(10_000).with_recovery_store(mismatch_store),
3107        )
3108        .await;
3109        assert!(matches!(
3110            mismatch_result,
3111            Err(ExactWorkflowError::Store {
3112                source: ExactWorkflowStoreError::IntentMismatch,
3113                ..
3114            })
3115        ));
3116
3117        let unavailable: Arc<dyn ExactWorkflowRecoveryStore> = Arc::new(UnavailableRecoveryStore);
3118        let unavailable_result = open_session(
3119            &intent,
3120            &ExactWorkflowOptions::new(10_000).with_recovery_store(unavailable),
3121        )
3122        .await;
3123        assert!(matches!(
3124            unavailable_result,
3125            Err(ExactWorkflowError::Store {
3126                source: ExactWorkflowStoreError::Unavailable,
3127                last_durable_recovery: None,
3128            })
3129        ));
3130        Ok(())
3131    }
3132
3133    #[tokio::test]
3134    async fn store_conflicts_and_cancellation_are_bounded_with_recovery() -> Result<(), Box<dyn Error>> {
3135        let intent = exact_intent(
3136            ExactMutationKindV1::Append,
3137            digest('b'),
3138            identity('c'),
3139            identity('d'),
3140            Some(resource()),
3141            digest('e'),
3142            u64::MAX,
3143        );
3144        let store: Arc<dyn ExactWorkflowRecoveryStore> = Arc::new(AdvancingConflictStore {
3145            row: Mutex::new(recovery_from_intent(&intent)),
3146        });
3147        let options = ExactWorkflowOptions::new(u64::MAX)
3148            .with_observation_deadline_unix_millis(u64::MAX)
3149            .with_store_conflict_limit(0)
3150            .with_recovery_store(store);
3151        let (session, mut recovery) = open_session(&intent, &options).await?;
3152        let conflict = session
3153            .record(
3154                &mut recovery,
3155                ExactMutationTransition::Prepared {
3156                    preparation: append_preparation_value(),
3157                },
3158            )
3159            .await;
3160        assert!(matches!(
3161            conflict,
3162            Err(ExactWorkflowError::StoreConflictBudgetExhausted {
3163                last_durable_recovery
3164            }) if last_durable_recovery.revision == 1
3165        ));
3166
3167        let cancellation = CallCancellation::new();
3168        cancellation.cancel();
3169        let store: Arc<dyn ExactWorkflowRecoveryStore> = Arc::new(PostgresShapedRecoveryStore::default());
3170        let options = ExactWorkflowOptions::new(u64::MAX)
3171            .with_cancellation(cancellation)
3172            .with_recovery_store(store);
3173        let (session, mut recovery) = open_session(&intent, &options).await?;
3174        let cancelled = session
3175            .record(
3176                &mut recovery,
3177                ExactMutationTransition::ClockObserved {
3178                    unix_millis: wire_u64(1),
3179                },
3180            )
3181            .await;
3182        assert!(matches!(
3183            cancelled,
3184            Err(ExactWorkflowError::Cancelled {
3185                last_durable_recovery
3186            }) if last_durable_recovery.revision == 0
3187        ));
3188        Ok(())
3189    }
3190
3191    #[tokio::test]
3192    async fn durable_visibility_polling_does_not_exhaust_recovery_records() -> Result<(), Box<dyn Error>> {
3193        let intent = exact_intent(
3194            ExactMutationKindV1::Append,
3195            digest('b'),
3196            identity('c'),
3197            identity('d'),
3198            Some(resource()),
3199            digest('e'),
3200            u64::MAX,
3201        );
3202        let store: Arc<dyn ExactWorkflowRecoveryStore> = Arc::new(PostgresShapedRecoveryStore::default());
3203        let clock: Arc<dyn ExactWorkflowClock> = Arc::new(AdvancingClock { next: Mutex::new(1) });
3204        let options = ExactWorkflowOptions::new(u64::MAX)
3205            .with_clock(clock)
3206            .with_recovery_store(Arc::clone(&store));
3207        let (session, mut recovery) = open_session(&intent, &options).await?;
3208
3209        assert_eq!(observe_clock(&session, &mut recovery, &options).await?, 1);
3210        assert!(
3211            session
3212                .record(
3213                    &mut recovery,
3214                    ExactMutationTransition::Prepared {
3215                        preparation: append_preparation_value(),
3216                    },
3217                )
3218                .await?
3219        );
3220        assert!(
3221            session
3222                .record(&mut recovery, ExactMutationTransition::SubmissionStarted)
3223                .await?
3224        );
3225        assert!(
3226            session
3227                .record(
3228                    &mut recovery,
3229                    ExactMutationTransition::SubmissionObserved {
3230                        disposition: ExactSubmissionDispositionV1::Resolved,
3231                    },
3232                )
3233                .await?
3234        );
3235        assert_eq!(recovery.revision, 4);
3236
3237        for expected_now in 2..=301 {
3238            assert_eq!(observe_clock(&session, &mut recovery, &options).await?, expected_now);
3239            record_resolution_observation(&session, &mut recovery, ExactSubmissionDispositionV1::Resolved).await?;
3240        }
3241        assert_eq!(recovery.revision, 4);
3242        assert_eq!(store.load().await?.map(|stored| stored.revision), Some(4));
3243        Ok(())
3244    }
3245
3246    #[tokio::test]
3247    async fn durable_resume_retains_the_first_effect_deadline() -> Result<(), Box<dyn Error>> {
3248        let mut suffix = [0_u8; 8];
3249        getrandom::fill(&mut suffix)?;
3250        let parent =
3251            std::fs::canonicalize(std::env::temp_dir())?.join(format!("lodestone-exact-{}", lower_hex(&suffix)));
3252        std::fs::create_dir(&parent)?;
3253        std::fs::set_permissions(&parent, std::fs::Permissions::from_mode(0o700))?;
3254        let path = parent.join("journal");
3255        let first_intent = exact_intent(
3256            ExactMutationKindV1::Append,
3257            digest('b'),
3258            identity('c'),
3259            identity('d'),
3260            Some(resource()),
3261            digest('e'),
3262            100,
3263        );
3264        {
3265            let store = Arc::new(FilesystemExactWorkflowRecoveryStore::new(path.clone()));
3266            let first = ExactWorkflowOptions::new(100).with_recovery_store(store);
3267            let (session, recovery) = open_session(&first_intent, &first).await?;
3268            assert_eq!(recovery_effect_deadline(&recovery)?, 100);
3269            drop(session);
3270        }
3271
3272        let resumed_intent = ExactMutationIntent {
3273            effect_deadline_unix_millis: wire_u64(999),
3274            ..first_intent
3275        };
3276        let store = Arc::new(FilesystemExactWorkflowRecoveryStore::new(path));
3277        let resumed = ExactWorkflowOptions::new(999).with_recovery_store(store);
3278        let (session, recovery) = open_session(&resumed_intent, &resumed).await?;
3279        assert_eq!(recovery_effect_deadline(&recovery)?, 100);
3280        drop(session);
3281        std::fs::remove_dir_all(&parent)?;
3282        Ok(())
3283    }
3284
3285    #[tokio::test]
3286    async fn filesystem_store_restarts_at_each_v2_durability_boundary() -> Result<(), Box<dyn Error>> {
3287        let mut suffix = [0_u8; 8];
3288        getrandom::fill(&mut suffix)?;
3289        let parent =
3290            std::fs::canonicalize(std::env::temp_dir())?.join(format!("lodestone-exact-v2-{}", lower_hex(&suffix)));
3291        std::fs::create_dir(&parent)?;
3292        std::fs::set_permissions(&parent, std::fs::Permissions::from_mode(0o700))?;
3293        let path = parent.join("journal");
3294        let intent = exact_intent(
3295            ExactMutationKindV1::Append,
3296            digest('b'),
3297            identity('c'),
3298            identity('d'),
3299            Some(resource()),
3300            digest('e'),
3301            10_000,
3302        );
3303        let mut state = recovery_from_intent(&intent);
3304        let store = FilesystemExactWorkflowRecoveryStore::new(path.clone());
3305        store.compare_exchange(None, state.clone()).await?;
3306        drop(store);
3307
3308        let transitions = [
3309            ExactMutationTransition::Prepared {
3310                preparation: append_preparation_value(),
3311            },
3312            ExactMutationTransition::SubmissionStarted,
3313            ExactMutationTransition::SubmissionObserved {
3314                disposition: ExactSubmissionDispositionV1::Indeterminate,
3315            },
3316            ExactMutationTransition::SubmissionObserved {
3317                disposition: ExactSubmissionDispositionV1::Resolved,
3318            },
3319            ExactMutationTransition::Terminal {
3320                result: ExactTerminalResultV1::Append(AppendExactResultV1 {
3321                    outcome: ExactMutationOutcomeV1::Resolved,
3322                    batch_id: digest('e'),
3323                    index: resource(),
3324                    items: Vec::new(),
3325                    visibility: Vec::new(),
3326                }),
3327            },
3328        ];
3329        for transition in transitions {
3330            let store = FilesystemExactWorkflowRecoveryStore::new(path.clone());
3331            assert_eq!(store.load().await?, Some(state.clone()));
3332            let mut next = state.clone();
3333            reduce_transition(&mut next, &transition)?;
3334            store.compare_exchange(Some(state.revision), next.clone()).await?;
3335            state = next;
3336            drop(store);
3337        }
3338        let store = FilesystemExactWorkflowRecoveryStore::new(path);
3339        let loaded = store.load().await?.ok_or("filesystem recovery disappeared")?;
3340        assert_eq!(loaded.phase, ExactWorkflowPhaseV2::Terminal);
3341        assert_eq!(loaded.caller_action_id, digest('e'));
3342        assert_eq!(
3343            append_preparation(&loaded)?.items[0]
3344                .locator
3345                .as_ref()
3346                .map(|value| value.value.as_str()),
3347            Some("01")
3348        );
3349        drop(store);
3350        std::fs::remove_dir_all(&parent)?;
3351        Ok(())
3352    }
3353}