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::path::{Path, PathBuf};
10use std::sync::Arc;
11use std::time::{SystemTime, UNIX_EPOCH};
12
13use aws_lc_rs::digest;
14use lodestone_wire::{
15    Digest, ErrorCode, GetIndexRequest, Identity, IndexResourceSpec, Locator, MutationOutcome,
16    ObserveAppendCommittedRequest, PrepareAppendBulkRequest, PrepareCreateIndexRequest, PrepareDocumentMutationRequest,
17    ResolveAppendBulkRequest, ResolveCatalogActionRequest, ResolveDocumentMutationRequest, Resource,
18    RetainedAppendBulkOutcome, RetainedAppendItem, RetainedAppendItemOutcome, RetainedAppendObservationOutcome,
19    RetainedAppendObservationState, RetainedAppendRetrySafety, RetainedAppendState, SubmitAppendBulkRequest,
20    SubmitCatalogActionRequest, SubmitDocumentMutationRequest, U64,
21};
22use serde::{Deserialize, Serialize};
23
24use crate::workflow_journal::sealed;
25use crate::{
26    CallCancellation, CallOptions, Client, ClientError, LocalOperatorWorkflowKind, SubmissionDisposition,
27    WorkflowJournal, WorkflowJournalError, WorkflowJournalIntent, WorkflowJournalTransition,
28};
29
30pub const EXACT_MUTATION_FORMAT_VERSION: u16 = 1;
31const MAX_EFFECT_CALL_MILLIS: u64 = 599_000;
32
33/// Returns the catalog submit discriminator for a prepared index creation.
34///
35/// The preparation authority selects retained-append creation whenever the
36/// caller explicitly requests retained event identity, so recovery must
37/// preserve that same discriminator at submit time.
38#[must_use]
39pub const fn create_index_action_kind(retained_event_identity: Option<bool>) -> &'static str {
40    if matches!(retained_event_identity, Some(true)) {
41        "create_retained_append_log_index"
42    } else {
43        "create_index"
44    }
45}
46
47/// Semantic owner of one recovery record.
48#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
49#[serde(rename_all = "snake_case")]
50pub enum ExactMutationKindV1 {
51    Append,
52    Document,
53    Index,
54}
55
56/// Closed result vocabulary shared by all exact mutation helpers.
57#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
58#[serde(rename_all = "snake_case")]
59pub enum ExactMutationOutcomeV1 {
60    AlreadySatisfied,
61    Applied,
62    Resolved,
63    Conflict,
64    NotSubmitted,
65    Indeterminate,
66    KnownRetired,
67    EffectDeadlineExpired,
68    ClockRegression,
69}
70
71#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
72#[serde(rename_all = "snake_case")]
73enum ExactSubmissionDispositionV1 {
74    NotSubmitted,
75    Submitted,
76    Resolved,
77    KnownRetired,
78    Indeterminate,
79}
80
81impl From<SubmissionDisposition> for ExactSubmissionDispositionV1 {
82    fn from(value: SubmissionDisposition) -> Self {
83        match value {
84            SubmissionDisposition::NotSubmitted => Self::NotSubmitted,
85            SubmissionDisposition::Submitted => Self::Submitted,
86            SubmissionDisposition::Resolved => Self::Resolved,
87            SubmissionDisposition::KnownRetired => Self::KnownRetired,
88            SubmissionDisposition::Indeterminate => Self::Indeterminate,
89        }
90    }
91}
92
93fn exact_submission_disposition(outcome: &MutationOutcome) -> ExactSubmissionDispositionV1 {
94    match outcome {
95        MutationOutcome::Terminal => ExactSubmissionDispositionV1::Resolved,
96        MutationOutcome::Pending { .. } => ExactSubmissionDispositionV1::Submitted,
97        MutationOutcome::Indeterminate { .. } | MutationOutcome::ResolutionRequired { .. } => {
98            ExactSubmissionDispositionV1::Indeterminate
99        }
100    }
101}
102
103/// Redacted retained-append item observation. Server error messages are never
104/// persisted in a recovery record.
105#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
106#[serde(deny_unknown_fields)]
107pub struct AppendExactItemResultV1 {
108    pub item_action_id: Digest,
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    pub locator: Option<Locator>,
111    #[serde(default, skip_serializing_if = "Option::is_none")]
112    pub semantic_digest: Option<Digest>,
113    pub state: RetainedAppendState,
114    pub retry_safety: RetainedAppendRetrySafety,
115    #[serde(default, skip_serializing_if = "Option::is_none")]
116    pub sequence: Option<U64>,
117    #[serde(default, skip_serializing_if = "Option::is_none")]
118    pub primary_term: Option<U64>,
119}
120
121#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
122#[serde(deny_unknown_fields)]
123pub struct AppendExactResultV1 {
124    pub outcome: ExactMutationOutcomeV1,
125    pub batch_id: Digest,
126    pub index: Resource,
127    pub items: Vec<AppendExactItemResultV1>,
128    #[serde(default, skip_serializing_if = "Vec::is_empty")]
129    pub visibility: Vec<RetainedAppendObservationOutcome>,
130}
131
132#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
133#[serde(deny_unknown_fields)]
134pub struct DocumentMutationExactResultV1 {
135    pub outcome: ExactMutationOutcomeV1,
136    pub index: Resource,
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub locator: Option<Locator>,
139    pub state: String,
140    pub submitted: bool,
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub sequence: Option<U64>,
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub mapping_generation: Option<U64>,
145    pub mapping_adopted: bool,
146}
147
148#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
149#[serde(deny_unknown_fields)]
150pub struct ReconcileIndexExactResultV1 {
151    pub outcome: ExactMutationOutcomeV1,
152    pub name: String,
153    #[serde(default, skip_serializing_if = "Option::is_none")]
154    pub locator: Option<Locator>,
155    #[serde(default, skip_serializing_if = "Option::is_none")]
156    pub resource: Option<Resource>,
157    pub state: String,
158}
159
160#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
161#[serde(tag = "kind", content = "result", rename_all = "snake_case", deny_unknown_fields)]
162enum ExactTerminalResultV1 {
163    Append(AppendExactResultV1),
164    Document(DocumentMutationExactResultV1),
165    Index(ReconcileIndexExactResultV1),
166}
167
168#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
169#[serde(tag = "kind", content = "value", rename_all = "snake_case", deny_unknown_fields)]
170enum ExactPreparationV1 {
171    Append(RetainedAppendBulkOutcome),
172    Document {
173        locator: Locator,
174        semantic_digest: Digest,
175    },
176    Index {
177        locator: Locator,
178        expires_at: U64,
179        semantic_digest: Digest,
180        resource_spec: Box<IndexResourceSpec>,
181    },
182}
183
184/// Non-secret state sufficient to resume the same effect. Callers that do not
185/// use the filesystem adapter must persist this value after an unresolved
186/// return.
187#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
188#[serde(deny_unknown_fields)]
189pub struct ExactWorkflowRecoveryV1 {
190    pub format_version: u16,
191    pub mutation_kind: ExactMutationKindV1,
192    pub request_sha256: Digest,
193    pub cluster_id: Identity,
194    pub collection_id: Identity,
195    #[serde(default, skip_serializing_if = "Option::is_none")]
196    pub index: Option<Resource>,
197    pub caller_action_id: Digest,
198    pub effect_deadline_unix_millis: U64,
199    #[serde(default, skip_serializing_if = "Option::is_none")]
200    pub last_observed_unix_millis: Option<U64>,
201    #[serde(default, skip_serializing_if = "Option::is_none")]
202    preparation: Option<ExactPreparationV1>,
203    #[serde(default, skip_serializing_if = "Option::is_none")]
204    submission_disposition: Option<ExactSubmissionDispositionV1>,
205    #[serde(default, skip_serializing_if = "Option::is_none")]
206    terminal: Option<ExactTerminalResultV1>,
207}
208
209/// One result plus the exact non-secret recovery state observed at return.
210#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
211#[serde(deny_unknown_fields)]
212pub struct ExactExecutionV1<T> {
213    pub result: T,
214    pub recovery: ExactWorkflowRecoveryV1,
215}
216
217/// Filesystem-backed adapter over Lodestone's sealed create-new workflow
218/// journal. The directory is exclusive to one exact request intent.
219#[derive(Clone, Debug, Eq, PartialEq)]
220pub struct ExactWorkflowStore {
221    path: PathBuf,
222}
223
224impl ExactWorkflowStore {
225    #[must_use]
226    pub fn new(path: impl Into<PathBuf>) -> Self {
227        Self { path: path.into() }
228    }
229
230    #[must_use]
231    pub fn path(&self) -> &Path {
232        &self.path
233    }
234}
235
236/// Injectable wall clock. Time is a liveness budget, never ordering or commit
237/// evidence.
238pub trait ExactWorkflowClock: Send + Sync {
239    fn unix_millis(&self) -> Result<u64, ExactWorkflowError>;
240}
241
242#[derive(Debug)]
243struct SystemExactWorkflowClock;
244
245impl ExactWorkflowClock for SystemExactWorkflowClock {
246    fn unix_millis(&self) -> Result<u64, ExactWorkflowError> {
247        let duration = SystemTime::now()
248            .duration_since(UNIX_EPOCH)
249            .map_err(|_| ExactWorkflowError::ClockUnavailable)?;
250        u64::try_from(duration.as_millis()).map_err(|_| ExactWorkflowError::ClockUnavailable)
251    }
252}
253
254/// Immutable budgets and optional recovery storage for one exact invocation.
255#[derive(Clone)]
256pub struct ExactWorkflowOptions {
257    effect_deadline_unix_millis: u64,
258    observation_deadline_unix_millis: Option<u64>,
259    store: Option<ExactWorkflowStore>,
260    recovery: Option<ExactWorkflowRecoveryV1>,
261    clock: Arc<dyn ExactWorkflowClock>,
262    cancellation: CallCancellation,
263}
264
265impl ExactWorkflowOptions {
266    #[must_use]
267    pub fn new(effect_deadline_unix_millis: u64) -> Self {
268        Self {
269            effect_deadline_unix_millis,
270            observation_deadline_unix_millis: None,
271            store: None,
272            recovery: None,
273            clock: Arc::new(SystemExactWorkflowClock),
274            cancellation: CallCancellation::new(),
275        }
276    }
277
278    #[must_use]
279    pub fn with_observation_deadline_unix_millis(mut self, value: u64) -> Self {
280        self.observation_deadline_unix_millis = Some(value);
281        self
282    }
283
284    #[must_use]
285    pub fn with_store(mut self, store: ExactWorkflowStore) -> Self {
286        self.store = Some(store);
287        self
288    }
289
290    #[must_use]
291    pub fn with_recovery(mut self, recovery: ExactWorkflowRecoveryV1) -> Self {
292        self.recovery = Some(recovery);
293        self
294    }
295
296    #[must_use]
297    pub fn with_clock(mut self, clock: Arc<dyn ExactWorkflowClock>) -> Self {
298        self.clock = clock;
299        self
300    }
301
302    #[must_use]
303    pub fn with_cancellation(mut self, cancellation: CallCancellation) -> Self {
304        self.cancellation = cancellation;
305        self
306    }
307}
308
309#[derive(Debug)]
310pub enum ExactWorkflowError {
311    InvalidInput,
312    InvalidRecovery,
313    RequestCodec,
314    CoreShape,
315    ClockUnavailable,
316    Journal(WorkflowJournalError),
317    Client(ClientError),
318}
319
320impl Display for ExactWorkflowError {
321    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
322        formatter.write_str(match self {
323            Self::InvalidInput => "the exact mutation input is invalid",
324            Self::InvalidRecovery => "the exact mutation recovery state does not match this request",
325            Self::RequestCodec => "the exact mutation request cannot be canonically encoded",
326            Self::CoreShape => "Core changed an exact mutation identity or outcome shape",
327            Self::ClockUnavailable => "the exact mutation wall clock is unavailable",
328            Self::Journal(_) => "the exact mutation journal could not be verified or updated",
329            Self::Client(_) => "the exact mutation client call failed",
330        })
331    }
332}
333
334impl Error for ExactWorkflowError {}
335
336impl From<WorkflowJournalError> for ExactWorkflowError {
337    fn from(value: WorkflowJournalError) -> Self {
338        Self::Journal(value)
339    }
340}
341
342/// Append request plus the optional visibility observation required by the
343/// caller. Its canonical encoding is bound into recovery state.
344#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
345#[serde(deny_unknown_fields)]
346pub struct AppendExactRequestV1 {
347    pub batch_id: Digest,
348    pub index: Resource,
349    pub items: Vec<RetainedAppendItem>,
350    #[serde(default)]
351    pub observe_committed: bool,
352}
353
354#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
355#[serde(deny_unknown_fields)]
356struct ExactMutationIntent {
357    format_version: u16,
358    mutation_kind: ExactMutationKindV1,
359    request_sha256: Digest,
360    cluster_id: Identity,
361    collection_id: Identity,
362    index: Option<Resource>,
363    caller_action_id: Digest,
364}
365
366impl sealed::Sealed for ExactMutationIntent {}
367
368impl WorkflowJournalIntent for ExactMutationIntent {
369    type Transition = ExactMutationTransition;
370
371    const KIND: LocalOperatorWorkflowKind = LocalOperatorWorkflowKind::ExactMutation;
372
373    fn validate(&self) -> Result<(), WorkflowJournalError> {
374        if self.format_version != EXACT_MUTATION_FORMAT_VERSION
375            || self.request_sha256.validate().is_err()
376            || self.cluster_id.validate().is_err()
377            || self.collection_id.validate().is_err()
378            || self.caller_action_id.validate().is_err()
379            || self.index.as_ref().is_some_and(|value| !valid_index(value))
380        {
381            return Err(WorkflowJournalError::InvalidIntent);
382        }
383        Ok(())
384    }
385}
386
387#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
388#[serde(tag = "transition", rename_all = "snake_case", deny_unknown_fields)]
389enum ExactMutationTransition {
390    DeadlineEstablished { unix_millis: U64 },
391    ClockObserved { unix_millis: U64 },
392    Prepared { preparation: ExactPreparationV1 },
393    SubmissionObserved { disposition: ExactSubmissionDispositionV1 },
394    Terminal { result: ExactTerminalResultV1 },
395}
396
397impl sealed::Sealed for ExactMutationTransition {}
398
399impl WorkflowJournalTransition for ExactMutationTransition {
400    fn validate(&self) -> Result<(), WorkflowJournalError> {
401        match self {
402            Self::DeadlineEstablished { unix_millis } if parse_u64(unix_millis).is_some_and(|value| value > 0) => {
403                Ok(())
404            }
405            Self::ClockObserved { unix_millis } if parse_u64(unix_millis).is_some() => Ok(()),
406            Self::Prepared { preparation } if valid_preparation(preparation) => Ok(()),
407            Self::SubmissionObserved { .. } => Ok(()),
408            Self::Terminal { result } if valid_terminal(result) => Ok(()),
409            _ => Err(WorkflowJournalError::InvalidTransition),
410        }
411    }
412}
413
414enum ExactSession {
415    Durable(WorkflowJournal<ExactMutationIntent>),
416    Ephemeral,
417}
418
419impl ExactSession {
420    async fn record(
421        &self,
422        state: &mut ExactWorkflowRecoveryV1,
423        transition: ExactMutationTransition,
424    ) -> Result<(), ExactWorkflowError> {
425        let mut next = state.clone();
426        reduce_transition(&mut next, &transition)?;
427        if let Self::Durable(journal) = self {
428            journal.append(transition).await?;
429        }
430        *state = next;
431        Ok(())
432    }
433}
434
435impl Client {
436    /// Runs retained append through prepare, submit, and resolver without ever
437    /// replacing the caller's batch or item action identities.
438    pub async fn append_exact(
439        &self,
440        request: AppendExactRequestV1,
441        options: ExactWorkflowOptions,
442    ) -> Result<ExactExecutionV1<AppendExactResultV1>, ExactWorkflowError> {
443        validate_append_request(&request)?;
444        let request_sha256 = canonical_sha256(&request)?;
445        let (cluster_id, collection_id) = self.exact_profile_scope();
446        let intent = exact_intent(
447            ExactMutationKindV1::Append,
448            request_sha256,
449            cluster_id,
450            collection_id,
451            Some(request.index.clone()),
452            request.batch_id.clone(),
453        );
454        let (session, mut recovery) = open_session(&intent, &options).await?;
455        let mut options = options;
456        options.effect_deadline_unix_millis = recovery_effect_deadline(&recovery)?;
457        if let Some(ExactTerminalResultV1::Append(result)) = &recovery.terminal {
458            return Ok(execution(result.clone(), recovery));
459        }
460        let now = observe_clock(&session, &mut recovery, &options).await?;
461        if clock_regressed(&recovery, now) {
462            return Ok(execution(
463                append_status(&request, &recovery, ExactMutationOutcomeV1::ClockRegression),
464                recovery,
465            ));
466        }
467
468        if recovery.preparation.is_none() {
469            if now >= options.effect_deadline_unix_millis {
470                let result = append_status(&request, &recovery, ExactMutationOutcomeV1::EffectDeadlineExpired);
471                return finish_append(&session, &mut recovery, result).await;
472            }
473            let prepared = self
474                .prepare_append_bulk(
475                    PrepareAppendBulkRequest {
476                        batch_id: request.batch_id.clone(),
477                        index: request.index.clone(),
478                        items: request.items.clone(),
479                    },
480                    effect_options(now, options.effect_deadline_unix_millis, &options)?,
481                )
482                .await
483                .map_err(ExactWorkflowError::Client)?
484                .outcome;
485            validate_append_outcome(&request, &prepared, true)?;
486            session
487                .record(
488                    &mut recovery,
489                    ExactMutationTransition::Prepared {
490                        preparation: ExactPreparationV1::Append(redact_append_outcome(prepared)),
491                    },
492                )
493                .await?;
494        }
495
496        let preparations = append_preparation(&recovery)?.items.clone();
497        let may_submit = !matches!(
498            recovery.submission_disposition,
499            Some(
500                ExactSubmissionDispositionV1::Submitted
501                    | ExactSubmissionDispositionV1::Resolved
502                    | ExactSubmissionDispositionV1::KnownRetired
503                    | ExactSubmissionDispositionV1::Indeterminate
504            )
505        );
506        if may_submit && now < options.effect_deadline_unix_millis {
507            match self
508                .submit_append_bulk(
509                    SubmitAppendBulkRequest {
510                        batch_id: request.batch_id.clone(),
511                        index: request.index.clone(),
512                        items: request.items.clone(),
513                        preparations,
514                    },
515                    effect_options(now, options.effect_deadline_unix_millis, &options)?,
516                )
517                .await
518            {
519                Ok(submitted) => {
520                    validate_append_outcome(&request, &submitted.outcome, false)?;
521                    let disposition = if submitted.outcome.items.iter().any(|item| {
522                        matches!(
523                            item.state,
524                            RetainedAppendState::Committed
525                                | RetainedAppendState::Replayed
526                                | RetainedAppendState::SubmittedIndeterminate
527                        )
528                    }) {
529                        ExactSubmissionDispositionV1::Submitted
530                    } else {
531                        ExactSubmissionDispositionV1::NotSubmitted
532                    };
533                    session
534                        .record(
535                            &mut recovery,
536                            ExactMutationTransition::SubmissionObserved { disposition },
537                        )
538                        .await?;
539                    if let Some(mut result) = append_terminal_result(&request, &submitted.outcome, false) {
540                        if !terminal_outcome(result.outcome) {
541                            return Ok(execution(result, recovery));
542                        }
543                        if request.observe_committed
544                            && matches!(
545                                result.outcome,
546                                ExactMutationOutcomeV1::Applied | ExactMutationOutcomeV1::Resolved
547                            )
548                        {
549                            let Some(observe_options) = resolution_options(now, &options)? else {
550                                result.outcome = ExactMutationOutcomeV1::Indeterminate;
551                                return Ok(execution(result, recovery));
552                            };
553                            result = observe_append_visibility(self, &request, result, observe_options).await?;
554                            if result.outcome == ExactMutationOutcomeV1::Indeterminate {
555                                return Ok(execution(result, recovery));
556                            }
557                        }
558                        return finish_append(&session, &mut recovery, result).await;
559                    }
560                }
561                Err(error) => {
562                    let disposition = ExactSubmissionDispositionV1::from(error.submission_disposition());
563                    session
564                        .record(
565                            &mut recovery,
566                            ExactMutationTransition::SubmissionObserved { disposition },
567                        )
568                        .await?;
569                    if disposition == ExactSubmissionDispositionV1::NotSubmitted {
570                        return Ok(execution(
571                            append_status(&request, &recovery, ExactMutationOutcomeV1::NotSubmitted),
572                            recovery,
573                        ));
574                    }
575                }
576            }
577        }
578
579        let Some(resolve_options) = resolution_options(now, &options)? else {
580            return Ok(execution(
581                append_status(&request, &recovery, ExactMutationOutcomeV1::Indeterminate),
582                recovery,
583            ));
584        };
585        let resolved = self
586            .resolve_append_bulk(
587                ResolveAppendBulkRequest {
588                    batch_id: request.batch_id.clone(),
589                    index: request.index.clone(),
590                    items: append_preparation(&recovery)?.items.clone(),
591                },
592                resolve_options,
593            )
594            .await;
595        let outcome = match resolved {
596            Ok(value) => value.outcome,
597            Err(_) => {
598                return Ok(execution(
599                    append_status(&request, &recovery, ExactMutationOutcomeV1::Indeterminate),
600                    recovery,
601                ));
602            }
603        };
604        validate_append_outcome(&request, &outcome, false)?;
605        let Some(mut result) = append_terminal_result(&request, &outcome, true) else {
606            return Ok(execution(
607                append_result(&request, &outcome, ExactMutationOutcomeV1::Indeterminate),
608                recovery,
609            ));
610        };
611        if request.observe_committed
612            && matches!(
613                result.outcome,
614                ExactMutationOutcomeV1::Applied | ExactMutationOutcomeV1::Resolved
615            )
616        {
617            let Some(observe_options) = resolution_options(now, &options)? else {
618                result.outcome = ExactMutationOutcomeV1::Indeterminate;
619                return Ok(execution(result, recovery));
620            };
621            result = observe_append_visibility(self, &request, result, observe_options).await?;
622            if result.outcome == ExactMutationOutcomeV1::Indeterminate {
623                return Ok(execution(result, recovery));
624            }
625        }
626        finish_append(&session, &mut recovery, result).await
627    }
628
629    /// Runs one document mutation with a stable prepared locator and resolves
630    /// any possibly submitted result before returning a terminal claim.
631    pub async fn mutate_document_exact(
632        &self,
633        request: PrepareDocumentMutationRequest,
634        options: ExactWorkflowOptions,
635    ) -> Result<ExactExecutionV1<DocumentMutationExactResultV1>, ExactWorkflowError> {
636        if !valid_index(&request.index) || request.document_id.is_empty() {
637            return Err(ExactWorkflowError::InvalidInput);
638        }
639        let request_sha256 = canonical_sha256(&request)?;
640        let (cluster_id, collection_id) = self.exact_profile_scope();
641        let intent = exact_intent(
642            ExactMutationKindV1::Document,
643            request_sha256.clone(),
644            cluster_id,
645            collection_id,
646            Some(request.index.clone()),
647            request_sha256,
648        );
649        let (session, mut recovery) = open_session(&intent, &options).await?;
650        let mut options = options;
651        options.effect_deadline_unix_millis = recovery_effect_deadline(&recovery)?;
652        if let Some(ExactTerminalResultV1::Document(result)) = &recovery.terminal {
653            return Ok(execution(result.clone(), recovery));
654        }
655        let now = observe_clock(&session, &mut recovery, &options).await?;
656        if clock_regressed(&recovery, now) {
657            return Ok(execution(
658                document_status(&request, &recovery, ExactMutationOutcomeV1::ClockRegression),
659                recovery,
660            ));
661        }
662        if recovery.preparation.is_none() {
663            if now >= options.effect_deadline_unix_millis {
664                let result = document_status(&request, &recovery, ExactMutationOutcomeV1::EffectDeadlineExpired);
665                return finish_document(&session, &mut recovery, result).await;
666            }
667            let prepared = self
668                .prepare_document_mutation(
669                    request.clone(),
670                    effect_options(now, options.effect_deadline_unix_millis, &options)?,
671                )
672                .await
673                .map_err(ExactWorkflowError::Client)?;
674            if prepared.locator.authority.is_empty() || prepared.semantic_digest.validate().is_err() {
675                return Err(ExactWorkflowError::CoreShape);
676            }
677            session
678                .record(
679                    &mut recovery,
680                    ExactMutationTransition::Prepared {
681                        preparation: ExactPreparationV1::Document {
682                            locator: prepared.locator,
683                            semantic_digest: prepared.semantic_digest,
684                        },
685                    },
686                )
687                .await?;
688        }
689        let (locator, semantic_digest) = document_preparation(&recovery)?;
690        let may_submit = !matches!(
691            recovery.submission_disposition,
692            Some(
693                ExactSubmissionDispositionV1::Submitted
694                    | ExactSubmissionDispositionV1::Resolved
695                    | ExactSubmissionDispositionV1::KnownRetired
696                    | ExactSubmissionDispositionV1::Indeterminate
697            )
698        );
699        if may_submit && now < options.effect_deadline_unix_millis {
700            match self
701                .submit_document_mutation(
702                    SubmitDocumentMutationRequest {
703                        locator: locator.clone(),
704                        semantic_digest,
705                        index: request.index.clone(),
706                        document_id: request.document_id.clone(),
707                        operation: request.operation.clone(),
708                        source: request.source.clone(),
709                        routing: request.routing.clone(),
710                        condition: request.condition.clone(),
711                        pipeline: request.pipeline.clone(),
712                    },
713                    effect_options(now, options.effect_deadline_unix_millis, &options)?,
714                )
715                .await
716            {
717                Ok(value) => {
718                    if value.locator != locator {
719                        return Err(ExactWorkflowError::CoreShape);
720                    }
721                    session
722                        .record(
723                            &mut recovery,
724                            ExactMutationTransition::SubmissionObserved {
725                                disposition: exact_submission_disposition(&value.mutation_outcome),
726                            },
727                        )
728                        .await?;
729                    if matches!(&value.mutation_outcome, MutationOutcome::Terminal)
730                        && matches!(value.state.as_str(), "applied" | "dropped")
731                    {
732                        let submitted = value.state == "applied";
733                        return finish_document(
734                            &session,
735                            &mut recovery,
736                            DocumentMutationExactResultV1 {
737                                outcome: ExactMutationOutcomeV1::Applied,
738                                index: request.index,
739                                locator: Some(value.locator),
740                                state: value.state,
741                                submitted,
742                                sequence: value.sequence,
743                                mapping_generation: value.mapping_generation,
744                                mapping_adopted: value.mapping_adopted,
745                            },
746                        )
747                        .await;
748                    }
749                }
750                Err(error) => {
751                    let disposition = ExactSubmissionDispositionV1::from(error.submission_disposition());
752                    session
753                        .record(
754                            &mut recovery,
755                            ExactMutationTransition::SubmissionObserved { disposition },
756                        )
757                        .await?;
758                    if disposition == ExactSubmissionDispositionV1::NotSubmitted {
759                        return Ok(execution(
760                            document_status(&request, &recovery, ExactMutationOutcomeV1::NotSubmitted),
761                            recovery,
762                        ));
763                    }
764                }
765            }
766        }
767        let Some(resolve_options) = resolution_options(now, &options)? else {
768            return Ok(execution(
769                document_status(&request, &recovery, ExactMutationOutcomeV1::Indeterminate),
770                recovery,
771            ));
772        };
773        let resolve_options = resolve_options
774            .with_index_name(request.index.name.clone())
775            .map_err(ExactWorkflowError::Client)?;
776        let value = match self
777            .resolve_document_mutation(
778                ResolveDocumentMutationRequest {
779                    locator: locator.clone(),
780                },
781                resolve_options,
782            )
783            .await
784        {
785            Ok(value) => value,
786            Err(_) => {
787                return Ok(execution(
788                    document_status(&request, &recovery, ExactMutationOutcomeV1::Indeterminate),
789                    recovery,
790                ));
791            }
792        };
793        if value.locator != locator || value.source.is_some() {
794            return Err(ExactWorkflowError::CoreShape);
795        }
796        let outcome = match value.state.as_str() {
797            "applied" => ExactMutationOutcomeV1::Resolved,
798            "failed" => ExactMutationOutcomeV1::Resolved,
799            "expired_unknown" => ExactMutationOutcomeV1::KnownRetired,
800            "prepared" => ExactMutationOutcomeV1::NotSubmitted,
801            "pending" => ExactMutationOutcomeV1::Indeterminate,
802            _ => return Err(ExactWorkflowError::CoreShape),
803        };
804        let result = DocumentMutationExactResultV1 {
805            outcome,
806            index: request.index,
807            locator: Some(value.locator),
808            state: value.state,
809            submitted: !matches!(outcome, ExactMutationOutcomeV1::NotSubmitted),
810            sequence: value.sequence,
811            mapping_generation: value.mapping_generation,
812            mapping_adopted: value.mapping_adopted,
813        };
814        if matches!(
815            outcome,
816            ExactMutationOutcomeV1::Indeterminate | ExactMutationOutcomeV1::NotSubmitted
817        ) {
818            Ok(execution(result, recovery))
819        } else {
820            finish_document(&session, &mut recovery, result).await
821        }
822    }
823
824    /// Reconciles only an absent index to one exact desired definition. An
825    /// existing different definition is a conflict; this helper never updates.
826    pub async fn reconcile_index_exact(
827        &self,
828        request: PrepareCreateIndexRequest,
829        options: ExactWorkflowOptions,
830    ) -> Result<ExactExecutionV1<ReconcileIndexExactResultV1>, ExactWorkflowError> {
831        if request.name.is_empty() || request.resource_spec.is_none() {
832            return Err(ExactWorkflowError::InvalidInput);
833        }
834        let request_sha256 = canonical_sha256(&request)?;
835        let (cluster_id, collection_id) = self.exact_profile_scope();
836        let intent = exact_intent(
837            ExactMutationKindV1::Index,
838            request_sha256.clone(),
839            cluster_id,
840            collection_id,
841            None,
842            request_sha256,
843        );
844        let (session, mut recovery) = open_session(&intent, &options).await?;
845        let mut options = options;
846        options.effect_deadline_unix_millis = recovery_effect_deadline(&recovery)?;
847        if let Some(ExactTerminalResultV1::Index(result)) = &recovery.terminal {
848            return Ok(execution(result.clone(), recovery));
849        }
850        let now = observe_clock(&session, &mut recovery, &options).await?;
851        if clock_regressed(&recovery, now) {
852            return Ok(execution(
853                index_status(&request, &recovery, ExactMutationOutcomeV1::ClockRegression),
854                recovery,
855            ));
856        }
857
858        if recovery.preparation.is_none() {
859            let Some(lookup_options) = resolution_options(now, &options)? else {
860                let result = index_status(&request, &recovery, ExactMutationOutcomeV1::EffectDeadlineExpired);
861                return finish_index(&session, &mut recovery, result).await;
862            };
863            let lookup = self
864                .get_index(
865                    GetIndexRequest {
866                        name: request.name.clone(),
867                        expected_incarnation: None,
868                    },
869                    lookup_options,
870                )
871                .await;
872            match lookup {
873                Ok(existing) => {
874                    let exact = existing.resource.current_mapping == request.mapping
875                        && request.resource_spec.as_ref() == Some(&existing.resource.resource_spec)
876                        && existing.resource.summary.retained_event_identity
877                            == request.retained_event_identity.unwrap_or(false);
878                    let outcome = if exact && existing.resource.summary.state == "active" {
879                        ExactMutationOutcomeV1::AlreadySatisfied
880                    } else if exact {
881                        ExactMutationOutcomeV1::Indeterminate
882                    } else {
883                        ExactMutationOutcomeV1::Conflict
884                    };
885                    let result = ReconcileIndexExactResultV1 {
886                        outcome,
887                        name: request.name,
888                        locator: None,
889                        resource: Some(Resource {
890                            kind: "index".to_owned(),
891                            name: existing.resource.summary.name,
892                            incarnation: existing.resource.summary.incarnation,
893                        }),
894                        state: existing.resource.summary.state,
895                    };
896                    if outcome == ExactMutationOutcomeV1::Indeterminate {
897                        return Ok(execution(result, recovery));
898                    }
899                    return finish_index(&session, &mut recovery, result).await;
900                }
901                Err(ClientError::Server(error)) if error.code == ErrorCode::NotFound => {}
902                Err(error) => return Err(ExactWorkflowError::Client(error)),
903            }
904            if now >= options.effect_deadline_unix_millis {
905                let result = index_status(&request, &recovery, ExactMutationOutcomeV1::EffectDeadlineExpired);
906                return finish_index(&session, &mut recovery, result).await;
907            }
908            let prepared = self
909                .prepare_create_index(
910                    request.clone(),
911                    effect_options(now, options.effect_deadline_unix_millis, &options)?,
912                )
913                .await
914                .map_err(ExactWorkflowError::Client)?;
915            if prepared.locator.authority.is_empty()
916                || prepared.semantic_digest.validate().is_err()
917                || prepared.resource_spec != *request.resource_spec.as_ref().ok_or(ExactWorkflowError::InvalidInput)?
918            {
919                return Err(ExactWorkflowError::CoreShape);
920            }
921            session
922                .record(
923                    &mut recovery,
924                    ExactMutationTransition::Prepared {
925                        preparation: ExactPreparationV1::Index {
926                            locator: prepared.locator,
927                            expires_at: prepared.expires_at,
928                            semantic_digest: prepared.semantic_digest,
929                            resource_spec: Box::new(prepared.resource_spec),
930                        },
931                    },
932                )
933                .await?;
934        }
935        let (locator, expires_at, semantic_digest, resource_spec) = index_preparation(&recovery)?;
936        let expiry = parse_u64(&expires_at).ok_or(ExactWorkflowError::InvalidRecovery)?;
937        let submit_deadline = options.effect_deadline_unix_millis.min(expiry);
938        let submission = SubmitCatalogActionRequest {
939            locator: locator.clone(),
940            semantic_digest,
941            action_kind: create_index_action_kind(request.retained_event_identity).to_owned(),
942            name: request.name.clone(),
943            mapping: Some(request.mapping.clone()),
944            resource_spec: Some(resource_spec),
945            expected_incarnation: None,
946        };
947        let may_submit = !matches!(
948            recovery.submission_disposition,
949            Some(
950                ExactSubmissionDispositionV1::Submitted
951                    | ExactSubmissionDispositionV1::Resolved
952                    | ExactSubmissionDispositionV1::KnownRetired
953                    | ExactSubmissionDispositionV1::Indeterminate
954            )
955        );
956        if may_submit && now < submit_deadline {
957            match self
958                .submit_catalog_action(submission.clone(), effect_options(now, submit_deadline, &options)?)
959                .await
960            {
961                Ok(value) => {
962                    if value.locator != locator {
963                        return Err(ExactWorkflowError::CoreShape);
964                    }
965                    session
966                        .record(
967                            &mut recovery,
968                            ExactMutationTransition::SubmissionObserved {
969                                disposition: exact_submission_disposition(&value.mutation_outcome),
970                            },
971                        )
972                        .await?;
973                }
974                Err(error) => {
975                    let disposition = ExactSubmissionDispositionV1::from(error.submission_disposition());
976                    session
977                        .record(
978                            &mut recovery,
979                            ExactMutationTransition::SubmissionObserved { disposition },
980                        )
981                        .await?;
982                    if disposition == ExactSubmissionDispositionV1::NotSubmitted {
983                        return Ok(execution(
984                            index_status(&request, &recovery, ExactMutationOutcomeV1::NotSubmitted),
985                            recovery,
986                        ));
987                    }
988                }
989            }
990        }
991        let Some(resolve_options) = resolution_options(now, &options)? else {
992            return Ok(execution(
993                index_status(&request, &recovery, ExactMutationOutcomeV1::Indeterminate),
994                recovery,
995            ));
996        };
997        let resolve_options = resolve_options
998            .with_index_name(request.name.clone())
999            .map_err(ExactWorkflowError::Client)?;
1000        let mut value = match self
1001            .resolve_catalog_action(
1002                ResolveCatalogActionRequest {
1003                    locator: locator.clone(),
1004                },
1005                resolve_options.clone(),
1006            )
1007            .await
1008        {
1009            Ok(value) => value,
1010            Err(_) => {
1011                return Ok(execution(
1012                    index_status(&request, &recovery, ExactMutationOutcomeV1::Indeterminate),
1013                    recovery,
1014                ));
1015            }
1016        };
1017        if value.locator != locator {
1018            return Err(ExactWorkflowError::CoreShape);
1019        }
1020        if catalog_resolution_requires_exact_replay(value.state.as_str(), recovery.submission_disposition) {
1021            // The owner has proved that this exact effect is already durably
1022            // admitted. Calling the same endpoint resumes resolution; it does
1023            // not authorize a first submission or extend the effect identity.
1024            match self.submit_catalog_action(submission, resolve_options.clone()).await {
1025                Ok(replayed) if replayed.locator == locator => {}
1026                Ok(_) => return Err(ExactWorkflowError::CoreShape),
1027                Err(error) if error.submission_disposition() == SubmissionDisposition::NotSubmitted => {
1028                    return Ok(execution(
1029                        index_status(&request, &recovery, ExactMutationOutcomeV1::NotSubmitted),
1030                        recovery,
1031                    ));
1032                }
1033                Err(_) => {
1034                    return Ok(execution(
1035                        index_status(&request, &recovery, ExactMutationOutcomeV1::Indeterminate),
1036                        recovery,
1037                    ));
1038                }
1039            }
1040            value = match self
1041                .resolve_catalog_action(
1042                    ResolveCatalogActionRequest {
1043                        locator: locator.clone(),
1044                    },
1045                    resolve_options,
1046                )
1047                .await
1048            {
1049                Ok(value) => value,
1050                Err(_) => {
1051                    return Ok(execution(
1052                        index_status(&request, &recovery, ExactMutationOutcomeV1::Indeterminate),
1053                        recovery,
1054                    ));
1055                }
1056            };
1057            if value.locator != locator {
1058                return Err(ExactWorkflowError::CoreShape);
1059            }
1060        }
1061        let outcome = match value.state.as_str() {
1062            "applied" => ExactMutationOutcomeV1::Resolved,
1063            "failed" => ExactMutationOutcomeV1::Conflict,
1064            "expired_unknown" => ExactMutationOutcomeV1::KnownRetired,
1065            "prepared" => ExactMutationOutcomeV1::NotSubmitted,
1066            "pending" => ExactMutationOutcomeV1::Indeterminate,
1067            _ => return Err(ExactWorkflowError::CoreShape),
1068        };
1069        let mut result = ReconcileIndexExactResultV1 {
1070            outcome,
1071            name: request.name,
1072            locator: Some(value.locator),
1073            resource: value.resource,
1074            state: value.state,
1075        };
1076        if outcome == ExactMutationOutcomeV1::Resolved {
1077            let Some(verify_options) = resolution_options(now, &options)? else {
1078                result.outcome = ExactMutationOutcomeV1::Indeterminate;
1079                return Ok(execution(result, recovery));
1080            };
1081            let existing = match self
1082                .get_index(
1083                    GetIndexRequest {
1084                        name: result.name.clone(),
1085                        expected_incarnation: result.resource.as_ref().map(|value| value.incarnation.clone()),
1086                    },
1087                    verify_options,
1088                )
1089                .await
1090            {
1091                Ok(value) => value.resource,
1092                Err(_) => {
1093                    result.outcome = ExactMutationOutcomeV1::Indeterminate;
1094                    return Ok(execution(result, recovery));
1095                }
1096            };
1097            if existing.current_mapping != request.mapping
1098                || request.resource_spec.as_ref() != Some(&existing.resource_spec)
1099                || existing.summary.retained_event_identity != request.retained_event_identity.unwrap_or(false)
1100            {
1101                result.outcome = ExactMutationOutcomeV1::Conflict;
1102                result.state = "definition_conflict".to_owned();
1103            } else if existing.summary.state != "active" {
1104                result.outcome = ExactMutationOutcomeV1::Indeterminate;
1105                result.state = existing.summary.state;
1106                return Ok(execution(result, recovery));
1107            } else {
1108                result.resource = Some(Resource {
1109                    kind: "index".to_owned(),
1110                    name: existing.summary.name,
1111                    incarnation: existing.summary.incarnation,
1112                });
1113                result.state = "active".to_owned();
1114            }
1115        }
1116        if matches!(
1117            outcome,
1118            ExactMutationOutcomeV1::Indeterminate | ExactMutationOutcomeV1::NotSubmitted
1119        ) {
1120            Ok(execution(result, recovery))
1121        } else {
1122            finish_index(&session, &mut recovery, result).await
1123        }
1124    }
1125
1126    fn exact_profile_scope(&self) -> (Identity, Identity) {
1127        let profile = self.connection_profile().as_wire();
1128        (
1129            profile.expected_cluster_id.clone(),
1130            profile.expected_collection_id.clone(),
1131        )
1132    }
1133}
1134
1135fn catalog_resolution_requires_exact_replay(state: &str, submission: Option<ExactSubmissionDispositionV1>) -> bool {
1136    state == "pending"
1137        || state == "prepared"
1138            && matches!(
1139                submission,
1140                Some(
1141                    ExactSubmissionDispositionV1::Submitted
1142                        | ExactSubmissionDispositionV1::Resolved
1143                        | ExactSubmissionDispositionV1::Indeterminate
1144                )
1145            )
1146}
1147
1148async fn observe_append_visibility(
1149    client: &Client,
1150    request: &AppendExactRequestV1,
1151    mut result: AppendExactResultV1,
1152    options: CallOptions,
1153) -> Result<AppendExactResultV1, ExactWorkflowError> {
1154    let mut visibility = Vec::with_capacity(result.items.len());
1155    for item in &result.items {
1156        let Some(locator) = item.locator.clone() else {
1157            return Err(ExactWorkflowError::CoreShape);
1158        };
1159        match client
1160            .observe_append_committed(
1161                ObserveAppendCommittedRequest {
1162                    index: request.index.clone(),
1163                    locator,
1164                },
1165                options.clone(),
1166            )
1167            .await
1168        {
1169            Ok(value) => visibility.push(value.outcome),
1170            Err(_) => {
1171                result.outcome = ExactMutationOutcomeV1::Indeterminate;
1172                result.visibility = visibility;
1173                return Ok(result);
1174            }
1175        }
1176    }
1177    if visibility
1178        .iter()
1179        .any(|value| value.state != RetainedAppendObservationState::Visible)
1180    {
1181        result.outcome = ExactMutationOutcomeV1::Indeterminate;
1182    }
1183    result.visibility = visibility;
1184    Ok(result)
1185}
1186
1187fn exact_intent(
1188    mutation_kind: ExactMutationKindV1,
1189    request_sha256: Digest,
1190    cluster_id: Identity,
1191    collection_id: Identity,
1192    index: Option<Resource>,
1193    caller_action_id: Digest,
1194) -> ExactMutationIntent {
1195    ExactMutationIntent {
1196        format_version: EXACT_MUTATION_FORMAT_VERSION,
1197        mutation_kind,
1198        request_sha256,
1199        cluster_id,
1200        collection_id,
1201        index,
1202        caller_action_id,
1203    }
1204}
1205
1206async fn open_session(
1207    intent: &ExactMutationIntent,
1208    options: &ExactWorkflowOptions,
1209) -> Result<(ExactSession, ExactWorkflowRecoveryV1), ExactWorkflowError> {
1210    intent.validate()?;
1211    if options.effect_deadline_unix_millis == 0 || options.store.is_some() && options.recovery.is_some() {
1212        return Err(ExactWorkflowError::InvalidRecovery);
1213    }
1214    let (session, mut recovery) = if let Some(store) = &options.store {
1215        let replay = WorkflowJournal::open_or_create(store.path(), intent.clone()).await?;
1216        let (journal, transitions) = replay.into_parts();
1217        let mut recovery = recovery_from_intent(intent);
1218        for transition in transitions {
1219            reduce_transition(&mut recovery, &transition)?;
1220        }
1221        (ExactSession::Durable(journal), recovery)
1222    } else {
1223        let recovery = match &options.recovery {
1224            Some(value) => {
1225                validate_recovery_matches(value, intent)?;
1226                value.clone()
1227            }
1228            None => recovery_from_intent(intent),
1229        };
1230        (ExactSession::Ephemeral, recovery)
1231    };
1232    if parse_u64(&recovery.effect_deadline_unix_millis) == Some(0) {
1233        session
1234            .record(
1235                &mut recovery,
1236                ExactMutationTransition::DeadlineEstablished {
1237                    unix_millis: wire_u64(options.effect_deadline_unix_millis),
1238                },
1239            )
1240            .await?;
1241    }
1242    Ok((session, recovery))
1243}
1244
1245fn recovery_from_intent(intent: &ExactMutationIntent) -> ExactWorkflowRecoveryV1 {
1246    ExactWorkflowRecoveryV1 {
1247        format_version: intent.format_version,
1248        mutation_kind: intent.mutation_kind,
1249        request_sha256: intent.request_sha256.clone(),
1250        cluster_id: intent.cluster_id.clone(),
1251        collection_id: intent.collection_id.clone(),
1252        index: intent.index.clone(),
1253        caller_action_id: intent.caller_action_id.clone(),
1254        effect_deadline_unix_millis: wire_u64(0),
1255        last_observed_unix_millis: None,
1256        preparation: None,
1257        submission_disposition: None,
1258        terminal: None,
1259    }
1260}
1261
1262fn validate_recovery_matches(
1263    recovery: &ExactWorkflowRecoveryV1,
1264    intent: &ExactMutationIntent,
1265) -> Result<(), ExactWorkflowError> {
1266    let expected = recovery_from_intent(intent);
1267    if recovery.format_version != expected.format_version
1268        || recovery.mutation_kind != expected.mutation_kind
1269        || recovery.request_sha256 != expected.request_sha256
1270        || recovery.cluster_id != expected.cluster_id
1271        || recovery.collection_id != expected.collection_id
1272        || recovery.index != expected.index
1273        || recovery.caller_action_id != expected.caller_action_id
1274        || parse_u64(&recovery.effect_deadline_unix_millis).is_none_or(|value| value == 0)
1275        || recovery
1276            .last_observed_unix_millis
1277            .as_ref()
1278            .is_some_and(|value| parse_u64(value).is_none())
1279        || recovery
1280            .preparation
1281            .as_ref()
1282            .is_some_and(|value| !valid_preparation(value))
1283        || recovery
1284            .preparation
1285            .as_ref()
1286            .is_some_and(|value| preparation_kind(value) != recovery.mutation_kind)
1287        || recovery.terminal.as_ref().is_some_and(|value| !valid_terminal(value))
1288        || recovery
1289            .terminal
1290            .as_ref()
1291            .is_some_and(|value| terminal_kind(value) != recovery.mutation_kind)
1292        || recovery.submission_disposition.is_some() && recovery.preparation.is_none()
1293    {
1294        return Err(ExactWorkflowError::InvalidRecovery);
1295    }
1296    Ok(())
1297}
1298
1299fn reduce_transition(
1300    state: &mut ExactWorkflowRecoveryV1,
1301    transition: &ExactMutationTransition,
1302) -> Result<(), ExactWorkflowError> {
1303    match transition {
1304        ExactMutationTransition::DeadlineEstablished { unix_millis }
1305            if parse_u64(&state.effect_deadline_unix_millis) == Some(0)
1306                && parse_u64(unix_millis).is_some_and(|value| value > 0) =>
1307        {
1308            state.effect_deadline_unix_millis = unix_millis.clone();
1309        }
1310        ExactMutationTransition::ClockObserved { unix_millis } => {
1311            let next = parse_u64(unix_millis).ok_or(ExactWorkflowError::InvalidRecovery)?;
1312            if state
1313                .last_observed_unix_millis
1314                .as_ref()
1315                .and_then(parse_u64)
1316                .is_some_and(|prior| next < prior)
1317            {
1318                return Err(ExactWorkflowError::InvalidRecovery);
1319            }
1320            state.last_observed_unix_millis = Some(unix_millis.clone());
1321        }
1322        ExactMutationTransition::Prepared { preparation }
1323            if state.preparation.is_none()
1324                && state.terminal.is_none()
1325                && preparation_kind(preparation) == state.mutation_kind =>
1326        {
1327            state.preparation = Some(preparation.clone());
1328        }
1329        ExactMutationTransition::SubmissionObserved { disposition }
1330            if state.preparation.is_some()
1331                && state.terminal.is_none()
1332                && !matches!(
1333                    state.submission_disposition,
1334                    Some(
1335                        ExactSubmissionDispositionV1::Submitted
1336                            | ExactSubmissionDispositionV1::Resolved
1337                            | ExactSubmissionDispositionV1::KnownRetired
1338                            | ExactSubmissionDispositionV1::Indeterminate
1339                    )
1340                ) =>
1341        {
1342            state.submission_disposition = Some(*disposition);
1343        }
1344        ExactMutationTransition::Terminal { result }
1345            if state.terminal.is_none() && terminal_kind(result) == state.mutation_kind =>
1346        {
1347            state.terminal = Some(result.clone());
1348        }
1349        _ => return Err(ExactWorkflowError::InvalidRecovery),
1350    }
1351    Ok(())
1352}
1353
1354async fn observe_clock(
1355    session: &ExactSession,
1356    recovery: &mut ExactWorkflowRecoveryV1,
1357    options: &ExactWorkflowOptions,
1358) -> Result<u64, ExactWorkflowError> {
1359    let now = options.clock.unix_millis()?;
1360    if recovery
1361        .last_observed_unix_millis
1362        .as_ref()
1363        .and_then(parse_u64)
1364        .is_some_and(|prior| now < prior)
1365    {
1366        return Ok(now);
1367    }
1368    session
1369        .record(
1370            recovery,
1371            ExactMutationTransition::ClockObserved {
1372                unix_millis: wire_u64(now),
1373            },
1374        )
1375        .await?;
1376    Ok(now)
1377}
1378
1379fn clock_regressed(recovery: &ExactWorkflowRecoveryV1, now: u64) -> bool {
1380    recovery
1381        .last_observed_unix_millis
1382        .as_ref()
1383        .and_then(parse_u64)
1384        .is_some_and(|observed| now < observed)
1385}
1386
1387fn recovery_effect_deadline(recovery: &ExactWorkflowRecoveryV1) -> Result<u64, ExactWorkflowError> {
1388    parse_u64(&recovery.effect_deadline_unix_millis)
1389        .filter(|value| *value > 0)
1390        .ok_or(ExactWorkflowError::InvalidRecovery)
1391}
1392
1393fn effect_options(now: u64, deadline: u64, options: &ExactWorkflowOptions) -> Result<CallOptions, ExactWorkflowError> {
1394    bounded_options(now, deadline, options)
1395}
1396
1397fn resolution_options(now: u64, options: &ExactWorkflowOptions) -> Result<Option<CallOptions>, ExactWorkflowError> {
1398    let deadline = if now < options.effect_deadline_unix_millis {
1399        options.effect_deadline_unix_millis
1400    } else {
1401        match options.observation_deadline_unix_millis {
1402            Some(value) if value > now => value,
1403            _ => return Ok(None),
1404        }
1405    };
1406    bounded_options(now, deadline, options).map(Some)
1407}
1408
1409fn bounded_options(now: u64, deadline: u64, options: &ExactWorkflowOptions) -> Result<CallOptions, ExactWorkflowError> {
1410    if deadline <= now {
1411        return Err(ExactWorkflowError::InvalidInput);
1412    }
1413    let bounded = deadline.min(now.saturating_add(MAX_EFFECT_CALL_MILLIS));
1414    CallOptions::from_absolute_deadline_unix_millis(bounded)
1415        .map(|value| value.with_cancellation(options.cancellation.clone()))
1416        .map_err(ExactWorkflowError::Client)
1417}
1418
1419fn validate_append_request(request: &AppendExactRequestV1) -> Result<(), ExactWorkflowError> {
1420    if request.batch_id.validate().is_err()
1421        || !valid_index(&request.index)
1422        || request.items.is_empty()
1423        || request.items.len() > 1_000
1424        || request.items.iter().any(|item| {
1425            item.item_action_id.validate().is_err() || item.document_id.is_empty() || item.document_id.len() > 512
1426        })
1427    {
1428        return Err(ExactWorkflowError::InvalidInput);
1429    }
1430    let mut identities = std::collections::BTreeSet::new();
1431    if request
1432        .items
1433        .iter()
1434        .any(|item| !identities.insert(item.item_action_id.value.as_str()))
1435    {
1436        return Err(ExactWorkflowError::InvalidInput);
1437    }
1438    Ok(())
1439}
1440
1441fn validate_append_outcome(
1442    request: &AppendExactRequestV1,
1443    outcome: &RetainedAppendBulkOutcome,
1444    prepared: bool,
1445) -> Result<(), ExactWorkflowError> {
1446    if outcome.batch_id != request.batch_id
1447        || outcome.index != request.index
1448        || outcome.items.len() != request.items.len()
1449    {
1450        return Err(ExactWorkflowError::CoreShape);
1451    }
1452    for (expected, observed) in request.items.iter().zip(&outcome.items) {
1453        if observed.item_action_id != expected.item_action_id
1454            || observed
1455                .locator
1456                .as_ref()
1457                .is_some_and(|value| value.authority.is_empty() || value.value.is_empty())
1458            || observed
1459                .semantic_digest
1460                .as_ref()
1461                .is_some_and(|value| value.validate().is_err())
1462            || prepared
1463                && (observed.state != RetainedAppendState::Prepared
1464                    || observed.retry_safety != RetainedAppendRetrySafety::Safe
1465                    || observed.locator.is_none()
1466                    || observed.semantic_digest.is_none()
1467                    || observed.sequence.is_some()
1468                    || observed.primary_term.is_some()
1469                    || observed.error.is_some())
1470        {
1471            return Err(ExactWorkflowError::CoreShape);
1472        }
1473    }
1474    Ok(())
1475}
1476
1477fn redact_append_outcome(mut outcome: RetainedAppendBulkOutcome) -> RetainedAppendBulkOutcome {
1478    for item in &mut outcome.items {
1479        item.error = None;
1480    }
1481    outcome
1482}
1483
1484fn append_preparation(recovery: &ExactWorkflowRecoveryV1) -> Result<&RetainedAppendBulkOutcome, ExactWorkflowError> {
1485    match &recovery.preparation {
1486        Some(ExactPreparationV1::Append(value)) => Ok(value),
1487        _ => Err(ExactWorkflowError::InvalidRecovery),
1488    }
1489}
1490
1491fn document_preparation(recovery: &ExactWorkflowRecoveryV1) -> Result<(Locator, Digest), ExactWorkflowError> {
1492    match &recovery.preparation {
1493        Some(ExactPreparationV1::Document {
1494            locator,
1495            semantic_digest,
1496        }) => Ok((locator.clone(), semantic_digest.clone())),
1497        _ => Err(ExactWorkflowError::InvalidRecovery),
1498    }
1499}
1500
1501fn index_preparation(
1502    recovery: &ExactWorkflowRecoveryV1,
1503) -> Result<(Locator, U64, Digest, IndexResourceSpec), ExactWorkflowError> {
1504    match &recovery.preparation {
1505        Some(ExactPreparationV1::Index {
1506            locator,
1507            expires_at,
1508            semantic_digest,
1509            resource_spec,
1510        }) => Ok((
1511            locator.clone(),
1512            expires_at.clone(),
1513            semantic_digest.clone(),
1514            resource_spec.as_ref().clone(),
1515        )),
1516        _ => Err(ExactWorkflowError::InvalidRecovery),
1517    }
1518}
1519
1520fn append_terminal_result(
1521    request: &AppendExactRequestV1,
1522    outcome: &RetainedAppendBulkOutcome,
1523    recovered: bool,
1524) -> Option<AppendExactResultV1> {
1525    let state = if outcome
1526        .items
1527        .iter()
1528        .any(|item| item.state == RetainedAppendState::SubmittedIndeterminate)
1529    {
1530        return None;
1531    } else if outcome
1532        .items
1533        .iter()
1534        .any(|item| item.state == RetainedAppendState::Conflict)
1535    {
1536        ExactMutationOutcomeV1::Conflict
1537    } else if outcome
1538        .items
1539        .iter()
1540        .any(|item| item.state == RetainedAppendState::Retired)
1541    {
1542        ExactMutationOutcomeV1::KnownRetired
1543    } else if outcome.items.iter().any(|item| {
1544        matches!(
1545            item.state,
1546            RetainedAppendState::NotSubmittedRetryable | RetainedAppendState::NotSubmittedTerminal
1547        )
1548    }) {
1549        ExactMutationOutcomeV1::NotSubmitted
1550    } else if outcome.items.iter().all(|item| {
1551        matches!(
1552            item.state,
1553            RetainedAppendState::Committed | RetainedAppendState::Replayed
1554        )
1555    }) {
1556        if recovered {
1557            ExactMutationOutcomeV1::Resolved
1558        } else {
1559            ExactMutationOutcomeV1::Applied
1560        }
1561    } else {
1562        return None;
1563    };
1564    Some(append_result(request, outcome, state))
1565}
1566
1567fn append_result(
1568    request: &AppendExactRequestV1,
1569    outcome: &RetainedAppendBulkOutcome,
1570    state: ExactMutationOutcomeV1,
1571) -> AppendExactResultV1 {
1572    AppendExactResultV1 {
1573        outcome: state,
1574        batch_id: request.batch_id.clone(),
1575        index: request.index.clone(),
1576        items: outcome.items.iter().map(redact_append_item).collect(),
1577        visibility: Vec::new(),
1578    }
1579}
1580
1581fn append_status(
1582    request: &AppendExactRequestV1,
1583    recovery: &ExactWorkflowRecoveryV1,
1584    state: ExactMutationOutcomeV1,
1585) -> AppendExactResultV1 {
1586    let items = append_preparation(recovery)
1587        .map(|value| value.items.iter().map(redact_append_item).collect())
1588        .unwrap_or_default();
1589    AppendExactResultV1 {
1590        outcome: state,
1591        batch_id: request.batch_id.clone(),
1592        index: request.index.clone(),
1593        items,
1594        visibility: Vec::new(),
1595    }
1596}
1597
1598fn redact_append_item(item: &RetainedAppendItemOutcome) -> AppendExactItemResultV1 {
1599    AppendExactItemResultV1 {
1600        item_action_id: item.item_action_id.clone(),
1601        locator: item.locator.clone(),
1602        semantic_digest: item.semantic_digest.clone(),
1603        state: item.state,
1604        retry_safety: item.retry_safety,
1605        sequence: item.sequence.clone(),
1606        primary_term: item.primary_term.clone(),
1607    }
1608}
1609
1610fn document_status(
1611    request: &PrepareDocumentMutationRequest,
1612    recovery: &ExactWorkflowRecoveryV1,
1613    outcome: ExactMutationOutcomeV1,
1614) -> DocumentMutationExactResultV1 {
1615    let locator = document_preparation(recovery).map(|value| value.0).ok();
1616    DocumentMutationExactResultV1 {
1617        outcome,
1618        index: request.index.clone(),
1619        locator,
1620        state: outcome_name(outcome).to_owned(),
1621        submitted: recovery
1622            .submission_disposition
1623            .is_some_and(|value| !matches!(value, ExactSubmissionDispositionV1::NotSubmitted)),
1624        sequence: None,
1625        mapping_generation: None,
1626        mapping_adopted: false,
1627    }
1628}
1629
1630fn index_status(
1631    request: &PrepareCreateIndexRequest,
1632    recovery: &ExactWorkflowRecoveryV1,
1633    outcome: ExactMutationOutcomeV1,
1634) -> ReconcileIndexExactResultV1 {
1635    let locator = match &recovery.preparation {
1636        Some(ExactPreparationV1::Index { locator, .. }) => Some(locator.clone()),
1637        _ => None,
1638    };
1639    ReconcileIndexExactResultV1 {
1640        outcome,
1641        name: request.name.clone(),
1642        locator,
1643        resource: None,
1644        state: outcome_name(outcome).to_owned(),
1645    }
1646}
1647
1648const fn outcome_name(outcome: ExactMutationOutcomeV1) -> &'static str {
1649    match outcome {
1650        ExactMutationOutcomeV1::AlreadySatisfied => "already_satisfied",
1651        ExactMutationOutcomeV1::Applied => "applied",
1652        ExactMutationOutcomeV1::Resolved => "resolved",
1653        ExactMutationOutcomeV1::Conflict => "conflict",
1654        ExactMutationOutcomeV1::NotSubmitted => "not_submitted",
1655        ExactMutationOutcomeV1::Indeterminate => "indeterminate",
1656        ExactMutationOutcomeV1::KnownRetired => "known_retired",
1657        ExactMutationOutcomeV1::EffectDeadlineExpired => "effect_deadline_expired",
1658        ExactMutationOutcomeV1::ClockRegression => "clock_regression",
1659    }
1660}
1661
1662async fn finish_append(
1663    session: &ExactSession,
1664    recovery: &mut ExactWorkflowRecoveryV1,
1665    result: AppendExactResultV1,
1666) -> Result<ExactExecutionV1<AppendExactResultV1>, ExactWorkflowError> {
1667    session
1668        .record(
1669            recovery,
1670            ExactMutationTransition::Terminal {
1671                result: ExactTerminalResultV1::Append(result.clone()),
1672            },
1673        )
1674        .await?;
1675    Ok(execution(result, recovery.clone()))
1676}
1677
1678async fn finish_document(
1679    session: &ExactSession,
1680    recovery: &mut ExactWorkflowRecoveryV1,
1681    result: DocumentMutationExactResultV1,
1682) -> Result<ExactExecutionV1<DocumentMutationExactResultV1>, ExactWorkflowError> {
1683    session
1684        .record(
1685            recovery,
1686            ExactMutationTransition::Terminal {
1687                result: ExactTerminalResultV1::Document(result.clone()),
1688            },
1689        )
1690        .await?;
1691    Ok(execution(result, recovery.clone()))
1692}
1693
1694async fn finish_index(
1695    session: &ExactSession,
1696    recovery: &mut ExactWorkflowRecoveryV1,
1697    result: ReconcileIndexExactResultV1,
1698) -> Result<ExactExecutionV1<ReconcileIndexExactResultV1>, ExactWorkflowError> {
1699    session
1700        .record(
1701            recovery,
1702            ExactMutationTransition::Terminal {
1703                result: ExactTerminalResultV1::Index(result.clone()),
1704            },
1705        )
1706        .await?;
1707    Ok(execution(result, recovery.clone()))
1708}
1709
1710fn execution<T>(result: T, recovery: ExactWorkflowRecoveryV1) -> ExactExecutionV1<T> {
1711    ExactExecutionV1 { result, recovery }
1712}
1713
1714fn valid_preparation(value: &ExactPreparationV1) -> bool {
1715    match value {
1716        ExactPreparationV1::Append(outcome) => {
1717            !outcome.items.is_empty()
1718                && outcome.items.len() <= 1_000
1719                && outcome.items.iter().all(|item| {
1720                    item.item_action_id.validate().is_ok()
1721                        && item
1722                            .locator
1723                            .as_ref()
1724                            .is_some_and(|locator| !locator.authority.is_empty() && !locator.value.is_empty())
1725                        && item
1726                            .semantic_digest
1727                            .as_ref()
1728                            .is_some_and(|digest| digest.validate().is_ok())
1729                        && item.state == RetainedAppendState::Prepared
1730                        && item.retry_safety == RetainedAppendRetrySafety::Safe
1731                        && item.error.is_none()
1732                })
1733        }
1734        ExactPreparationV1::Document {
1735            locator,
1736            semantic_digest,
1737        } => !locator.authority.is_empty() && !locator.value.is_empty() && semantic_digest.validate().is_ok(),
1738        ExactPreparationV1::Index {
1739            locator,
1740            expires_at,
1741            semantic_digest,
1742            ..
1743        } => {
1744            !locator.authority.is_empty()
1745                && !locator.value.is_empty()
1746                && parse_u64(expires_at).is_some_and(|value| value > 0)
1747                && semantic_digest.validate().is_ok()
1748        }
1749    }
1750}
1751
1752fn valid_terminal(value: &ExactTerminalResultV1) -> bool {
1753    match value {
1754        ExactTerminalResultV1::Append(result) => {
1755            result.batch_id.validate().is_ok()
1756                && valid_index(&result.index)
1757                && terminal_outcome(result.outcome)
1758                && result.items.iter().all(|item| item.item_action_id.validate().is_ok())
1759        }
1760        ExactTerminalResultV1::Document(result) => {
1761            valid_index(&result.index)
1762                && result
1763                    .locator
1764                    .as_ref()
1765                    .is_none_or(|locator| !locator.authority.is_empty() && !locator.value.is_empty())
1766                && terminal_outcome(result.outcome)
1767        }
1768        ExactTerminalResultV1::Index(result) => !result.name.is_empty() && terminal_outcome(result.outcome),
1769    }
1770}
1771
1772const fn terminal_outcome(value: ExactMutationOutcomeV1) -> bool {
1773    matches!(
1774        value,
1775        ExactMutationOutcomeV1::AlreadySatisfied
1776            | ExactMutationOutcomeV1::Applied
1777            | ExactMutationOutcomeV1::Resolved
1778            | ExactMutationOutcomeV1::Conflict
1779            | ExactMutationOutcomeV1::KnownRetired
1780            | ExactMutationOutcomeV1::EffectDeadlineExpired
1781    )
1782}
1783
1784const fn preparation_kind(value: &ExactPreparationV1) -> ExactMutationKindV1 {
1785    match value {
1786        ExactPreparationV1::Append(_) => ExactMutationKindV1::Append,
1787        ExactPreparationV1::Document { .. } => ExactMutationKindV1::Document,
1788        ExactPreparationV1::Index { .. } => ExactMutationKindV1::Index,
1789    }
1790}
1791
1792const fn terminal_kind(value: &ExactTerminalResultV1) -> ExactMutationKindV1 {
1793    match value {
1794        ExactTerminalResultV1::Append(_) => ExactMutationKindV1::Append,
1795        ExactTerminalResultV1::Document(_) => ExactMutationKindV1::Document,
1796        ExactTerminalResultV1::Index(_) => ExactMutationKindV1::Index,
1797    }
1798}
1799
1800fn valid_index(value: &Resource) -> bool {
1801    value.kind == "index" && !value.name.is_empty() && value.name.len() <= 255 && value.incarnation.validate().is_ok()
1802}
1803
1804fn canonical_sha256<T: Serialize>(value: &T) -> Result<Digest, ExactWorkflowError> {
1805    let bytes = serde_json::to_vec(value).map_err(|_| ExactWorkflowError::RequestCodec)?;
1806    let observed = digest::digest(&digest::SHA256, &bytes);
1807    Ok(Digest {
1808        value: lower_hex(observed.as_ref()),
1809    })
1810}
1811
1812fn parse_u64(value: &U64) -> Option<u64> {
1813    if value.value.is_empty() || value.value.len() > 20 || value.value.starts_with('0') && value.value != "0" {
1814        return None;
1815    }
1816    value.value.parse().ok()
1817}
1818
1819fn wire_u64(value: u64) -> U64 {
1820    U64 {
1821        value: value.to_string(),
1822    }
1823}
1824
1825fn lower_hex(bytes: &[u8]) -> String {
1826    const HEX: &[u8; 16] = b"0123456789abcdef";
1827    let mut output = String::with_capacity(bytes.len().saturating_mul(2));
1828    for byte in bytes {
1829        output.push(char::from(HEX[usize::from(byte >> 4)]));
1830        output.push(char::from(HEX[usize::from(byte & 0x0f)]));
1831    }
1832    output
1833}
1834
1835#[cfg(test)]
1836mod tests {
1837    use std::os::unix::fs::PermissionsExt as _;
1838
1839    use super::*;
1840
1841    fn identity(character: char) -> Identity {
1842        Identity {
1843            value: character.to_string().repeat(32),
1844        }
1845    }
1846
1847    fn digest(character: char) -> Digest {
1848        Digest {
1849            value: character.to_string().repeat(64),
1850        }
1851    }
1852
1853    fn resource() -> Resource {
1854        Resource {
1855            kind: "index".to_owned(),
1856            name: "events".to_owned(),
1857            incarnation: identity('a'),
1858        }
1859    }
1860
1861    #[test]
1862    fn retained_event_identity_preserves_the_catalog_submit_kind() {
1863        assert_eq!(create_index_action_kind(None), "create_index");
1864        assert_eq!(create_index_action_kind(Some(false)), "create_index");
1865        assert_eq!(create_index_action_kind(Some(true)), "create_retained_append_log_index");
1866    }
1867
1868    #[test]
1869    fn catalog_recovery_replays_only_an_exact_prepared_or_pending_identity() {
1870        assert!(catalog_resolution_requires_exact_replay("pending", None));
1871        assert!(catalog_resolution_requires_exact_replay(
1872            "prepared",
1873            Some(ExactSubmissionDispositionV1::Indeterminate),
1874        ));
1875        assert!(catalog_resolution_requires_exact_replay(
1876            "prepared",
1877            Some(ExactSubmissionDispositionV1::Submitted),
1878        ));
1879        assert!(!catalog_resolution_requires_exact_replay("prepared", None));
1880        assert!(!catalog_resolution_requires_exact_replay(
1881            "prepared",
1882            Some(ExactSubmissionDispositionV1::NotSubmitted),
1883        ));
1884        assert!(!catalog_resolution_requires_exact_replay(
1885            "expired_unknown",
1886            Some(ExactSubmissionDispositionV1::Indeterminate),
1887        ));
1888    }
1889
1890    #[test]
1891    fn recovery_refuses_changed_request_or_scope_and_retains_first_deadline() -> Result<(), Box<dyn Error>> {
1892        let intent = exact_intent(
1893            ExactMutationKindV1::Append,
1894            digest('b'),
1895            identity('c'),
1896            identity('d'),
1897            Some(resource()),
1898            digest('e'),
1899        );
1900        let mut recovery = recovery_from_intent(&intent);
1901        reduce_transition(
1902            &mut recovery,
1903            &ExactMutationTransition::DeadlineEstablished {
1904                unix_millis: wire_u64(100),
1905            },
1906        )?;
1907        validate_recovery_matches(&recovery, &intent)?;
1908        assert_eq!(recovery_effect_deadline(&recovery)?, 100);
1909        let mut changed = intent.clone();
1910        changed.collection_id = identity('f');
1911        assert!(matches!(
1912            validate_recovery_matches(&recovery, &changed),
1913            Err(ExactWorkflowError::InvalidRecovery)
1914        ));
1915        changed = intent.clone();
1916        changed.request_sha256 = digest('f');
1917        assert!(matches!(
1918            validate_recovery_matches(&recovery, &changed),
1919            Err(ExactWorkflowError::InvalidRecovery)
1920        ));
1921        Ok(())
1922    }
1923
1924    #[test]
1925    fn reducer_never_regresses_clock_or_replaces_possible_submission() -> Result<(), Box<dyn Error>> {
1926        let intent = exact_intent(
1927            ExactMutationKindV1::Append,
1928            digest('b'),
1929            identity('c'),
1930            identity('d'),
1931            Some(resource()),
1932            digest('e'),
1933        );
1934        let mut state = recovery_from_intent(&intent);
1935        reduce_transition(
1936            &mut state,
1937            &ExactMutationTransition::DeadlineEstablished {
1938                unix_millis: wire_u64(100),
1939            },
1940        )?;
1941        reduce_transition(
1942            &mut state,
1943            &ExactMutationTransition::ClockObserved {
1944                unix_millis: wire_u64(10),
1945            },
1946        )?;
1947        assert!(
1948            reduce_transition(
1949                &mut state,
1950                &ExactMutationTransition::ClockObserved {
1951                    unix_millis: wire_u64(9),
1952                },
1953            )
1954            .is_err()
1955        );
1956        state.preparation = Some(ExactPreparationV1::Append(RetainedAppendBulkOutcome {
1957            batch_id: digest('e'),
1958            index: resource(),
1959            items: vec![RetainedAppendItemOutcome {
1960                item_action_id: digest('f'),
1961                locator: Some(Locator {
1962                    authority: "retained_append".to_owned(),
1963                    value: "01".to_owned(),
1964                }),
1965                semantic_digest: Some(digest('1')),
1966                state: RetainedAppendState::Prepared,
1967                retry_safety: RetainedAppendRetrySafety::Safe,
1968                sequence: None,
1969                primary_term: None,
1970                error: None,
1971            }],
1972            complete: true,
1973            achievement: None,
1974        }));
1975        reduce_transition(
1976            &mut state,
1977            &ExactMutationTransition::SubmissionObserved {
1978                disposition: ExactSubmissionDispositionV1::Indeterminate,
1979            },
1980        )?;
1981        assert!(
1982            reduce_transition(
1983                &mut state,
1984                &ExactMutationTransition::SubmissionObserved {
1985                    disposition: ExactSubmissionDispositionV1::NotSubmitted,
1986                },
1987            )
1988            .is_err()
1989        );
1990        Ok(())
1991    }
1992
1993    #[test]
1994    fn not_submitted_append_outcome_remains_recovery_resumable() -> Result<(), Box<dyn Error>> {
1995        let request = AppendExactRequestV1 {
1996            batch_id: digest('a'),
1997            index: resource(),
1998            items: vec![RetainedAppendItem {
1999                item_action_id: digest('b'),
2000                document_id: "event-1".to_owned(),
2001                source: lodestone_wire::RawJson {
2002                    bytes_base64: "e30=".to_owned(),
2003                },
2004            }],
2005            observe_committed: true,
2006        };
2007        let outcome = RetainedAppendBulkOutcome {
2008            batch_id: request.batch_id.clone(),
2009            index: request.index.clone(),
2010            items: vec![RetainedAppendItemOutcome {
2011                item_action_id: request.items[0].item_action_id.clone(),
2012                locator: Some(Locator {
2013                    authority: "retained_append".to_owned(),
2014                    value: "01".to_owned(),
2015                }),
2016                semantic_digest: Some(digest('c')),
2017                state: RetainedAppendState::NotSubmittedRetryable,
2018                retry_safety: RetainedAppendRetrySafety::Safe,
2019                sequence: None,
2020                primary_term: None,
2021                error: None,
2022            }],
2023            complete: false,
2024            achievement: None,
2025        };
2026
2027        let result = append_terminal_result(&request, &outcome, false).ok_or("not-submitted observation absent")?;
2028        assert_eq!(result.outcome, ExactMutationOutcomeV1::NotSubmitted);
2029        assert!(!terminal_outcome(result.outcome));
2030        Ok(())
2031    }
2032
2033    #[test]
2034    fn append_recovery_redacts_server_error_messages() {
2035        let outcome = RetainedAppendBulkOutcome {
2036            batch_id: digest('a'),
2037            index: resource(),
2038            items: vec![RetainedAppendItemOutcome {
2039                item_action_id: digest('b'),
2040                locator: Some(Locator {
2041                    authority: "retained_append".to_owned(),
2042                    value: "01".to_owned(),
2043                }),
2044                semantic_digest: Some(digest('c')),
2045                state: RetainedAppendState::Prepared,
2046                retry_safety: RetainedAppendRetrySafety::Safe,
2047                sequence: None,
2048                primary_term: None,
2049                error: None,
2050            }],
2051            complete: true,
2052            achievement: None,
2053        };
2054        assert!(redact_append_outcome(outcome).items[0].error.is_none());
2055    }
2056
2057    #[tokio::test]
2058    async fn durable_resume_retains_the_first_effect_deadline() -> Result<(), Box<dyn Error>> {
2059        let mut suffix = [0_u8; 8];
2060        getrandom::fill(&mut suffix)?;
2061        let parent =
2062            std::fs::canonicalize(std::env::temp_dir())?.join(format!("lodestone-exact-{}", lower_hex(&suffix)));
2063        std::fs::create_dir(&parent)?;
2064        std::fs::set_permissions(&parent, std::fs::Permissions::from_mode(0o700))?;
2065        let store = ExactWorkflowStore::new(parent.join("journal"));
2066        let intent = exact_intent(
2067            ExactMutationKindV1::Append,
2068            digest('b'),
2069            identity('c'),
2070            identity('d'),
2071            Some(resource()),
2072            digest('e'),
2073        );
2074        let first = ExactWorkflowOptions::new(100).with_store(store.clone());
2075        let (session, recovery) = open_session(&intent, &first).await?;
2076        assert_eq!(recovery_effect_deadline(&recovery)?, 100);
2077        drop(session);
2078
2079        let resumed = ExactWorkflowOptions::new(999).with_store(store);
2080        let (session, recovery) = open_session(&intent, &resumed).await?;
2081        assert_eq!(recovery_effect_deadline(&recovery)?, 100);
2082        drop(session);
2083        std::fs::remove_dir_all(&parent)?;
2084        Ok(())
2085    }
2086}