Skip to main content

acyclic_fs/
workspace.rs

1//! Customer workspace identities and immutable generation handles.
2//!
3//! A workspace is the sole customer-visible unit of mutation, publication,
4//! retention, and convergence. The engine's volume and authority identities
5//! remain implementation details.
6
7use crate::foundation::{FileId, GenerationId, OperationId, VolumeId};
8use crate::kernel::{
9    ExtentKind, FileKind, FileMetadata, FilePayload, LogicalName, MetadataField, NamespacePath,
10};
11use crate::model::{CheckoutMode, GenerationSelector};
12use crate::path::PortablePath;
13use crate::{
14    AsyncAuthorityStore, AsyncObjectStore, AuthoredMutation, Checkout, CheckoutCommitOutcome,
15    ConflictKey, ConflictKind, ConflictSide, ConflictValue, FsError, GenerationDiff, MergeConflict,
16    MergeDriverRegistry, MergePlan, MergePlanResolutionError, MergeResolution,
17    MergeResolutionCache, UnpublishedMergeCandidate, Volume, resolve_merge_plan,
18};
19use bytes::Bytes;
20use serde::{Deserialize, Serialize};
21use std::collections::{BTreeMap, BTreeSet};
22use std::fmt;
23use std::sync::Arc;
24use thiserror::Error;
25use unicode_normalization::UnicodeNormalization;
26
27const WORKSPACE_ID_DOMAIN: &[u8] = b"acyclic-fs-workspace-id-v1\0";
28const MAXIMUM_WORKSPACE_NAME_BYTES: usize = 255;
29
30/// Canonical immutable customer workspace name.
31///
32/// Names are NFC-normalized, case-sensitive UTF-8 and cannot be reused for a
33/// different workspace in one filesystem namespace.
34#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
35pub struct WorkspaceName(Arc<str>);
36
37impl WorkspaceName {
38    /// Canonicalizes and validates one friendly workspace name.
39    ///
40    /// # Errors
41    ///
42    /// Rejects empty, reserved, path-like, control-containing, or oversized
43    /// names before deriving any durable identity.
44    pub fn new(name: impl AsRef<str>) -> Result<Self, WorkspaceNameError> {
45        let canonical: String = name.as_ref().nfc().collect();
46        if canonical.is_empty() {
47            return Err(WorkspaceNameError::Empty);
48        }
49        if canonical == "." || canonical == ".." {
50            return Err(WorkspaceNameError::Reserved);
51        }
52        if canonical.len() > MAXIMUM_WORKSPACE_NAME_BYTES {
53            return Err(WorkspaceNameError::TooLong);
54        }
55        if canonical
56            .chars()
57            .any(|character| character.is_control() || matches!(character, '/' | '\\'))
58        {
59            return Err(WorkspaceNameError::InvalidCharacter);
60        }
61        Ok(Self(Arc::from(canonical)))
62    }
63
64    /// Returns the canonical customer spelling.
65    #[must_use]
66    pub fn as_str(&self) -> &str {
67        &self.0
68    }
69}
70
71impl fmt::Debug for WorkspaceName {
72    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
73        formatter
74            .debug_tuple("WorkspaceName")
75            .field(&self.0)
76            .finish()
77    }
78}
79
80impl fmt::Display for WorkspaceName {
81    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
82        formatter.write_str(self.as_str())
83    }
84}
85
86/// Stable opaque identity of one named workspace.
87#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
88#[serde(transparent)]
89pub struct WorkspaceId([u8; 16]);
90
91impl WorkspaceId {
92    pub(crate) const fn from_volume_id(volume_id: VolumeId) -> Self {
93        Self(volume_id.into_bytes())
94    }
95
96    /// Restores a stable workspace identity from canonical bytes.
97    #[must_use]
98    pub const fn from_bytes(bytes: [u8; 16]) -> Self {
99        Self(bytes)
100    }
101
102    /// Derives the stable identity for a canonical name in one deployment
103    /// namespace. The namespace is deliberately internal to the selected
104    /// `Fs` deployment.
105    #[must_use]
106    pub(crate) fn derive(namespace: [u8; 16], name: &WorkspaceName) -> Self {
107        let mut hasher = blake3::Hasher::new();
108        hasher.update(WORKSPACE_ID_DOMAIN);
109        hasher.update(&namespace);
110        hasher.update(
111            &u64::try_from(name.as_str().len())
112                .unwrap_or(u64::MAX)
113                .to_le_bytes(),
114        );
115        hasher.update(name.as_str().as_bytes());
116        let mut bytes = [0_u8; 16];
117        bytes.copy_from_slice(&hasher.finalize().as_bytes()[..16]);
118        Self(bytes)
119    }
120
121    /// Returns the stable opaque bytes for persistence or transport.
122    #[must_use]
123    pub const fn into_bytes(self) -> [u8; 16] {
124        self.0
125    }
126
127    pub(crate) const fn volume_id(self) -> VolumeId {
128        VolumeId::from_bytes(self.0)
129    }
130}
131
132/// Stable retry identity for one customer-visible mutation, fork, or join.
133#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
134#[serde(transparent)]
135pub struct IdempotencyKey([u8; 16]);
136
137impl IdempotencyKey {
138    /// Creates a fresh time-ordered retry identity.
139    #[must_use]
140    pub fn new() -> Self {
141        Self(OperationId::new().into_bytes())
142    }
143
144    /// Restores an exact key after an ambiguous outcome.
145    #[must_use]
146    pub const fn from_bytes(bytes: [u8; 16]) -> Self {
147        Self(bytes)
148    }
149
150    /// Returns the stable wire bytes.
151    #[must_use]
152    pub const fn into_bytes(self) -> [u8; 16] {
153        self.0
154    }
155
156    pub(crate) const fn operation_id(self) -> OperationId {
157        OperationId::from_bytes(self.0)
158    }
159}
160
161impl Default for IdempotencyKey {
162    fn default() -> Self {
163        Self::new()
164    }
165}
166
167/// One named mutable filesystem head.
168pub struct Workspace<A, O> {
169    pub(crate) name: WorkspaceName,
170    pub(crate) id: WorkspaceId,
171    pub(crate) volume: Volume<A, O>,
172    #[cfg(all(feature = "native-watch", not(target_arch = "wasm32")))]
173    pub(crate) source: Option<crate::Source<A, O>>,
174}
175
176impl<A, O> Clone for Workspace<A, O> {
177    fn clone(&self) -> Self {
178        Self {
179            name: self.name.clone(),
180            id: self.id,
181            volume: self.volume.clone(),
182            #[cfg(all(feature = "native-watch", not(target_arch = "wasm32")))]
183            source: self.source.clone(),
184        }
185    }
186}
187
188impl<A, O> Workspace<A, O> {
189    /// Stable opaque workspace identity.
190    #[must_use]
191    pub const fn id(&self) -> WorkspaceId {
192        self.id
193    }
194
195    pub(crate) fn authority(&self) -> &A {
196        self.volume.fs.authority()
197    }
198
199    pub(crate) const fn authority_id(&self) -> crate::AuthorityId {
200        crate::kernel::volume_authority_id(self.id.volume_id())
201    }
202
203    /// Immutable canonical workspace name.
204    #[must_use]
205    pub fn name(&self) -> &WorkspaceName {
206        &self.name
207    }
208
209    /// Derives the stable identity of a sibling workspace before creating it.
210    ///
211    /// Durable orchestration stores use this to record a recoverable fork
212    /// intent before the child authority is created.
213    pub fn fork_workspace_id(
214        &self,
215        destination: impl AsRef<str>,
216    ) -> Result<WorkspaceId, WorkspaceNameError> {
217        self.volume.fs.workspace_id(destination)
218    }
219
220    /// Exact immutable filesystem profile selected when this workspace was created.
221    #[must_use]
222    pub const fn profile(&self) -> crate::model::FilesystemProfile {
223        self.volume.config.profile
224    }
225
226    /// Exact immutable bounds selected for this workspace volume.
227    #[must_use]
228    pub const fn limits(&self) -> crate::model::VolumeLimits {
229        self.volume.config.limits
230    }
231}
232
233impl<A: AsyncAuthorityStore, O: AsyncObjectStore> Workspace<A, O> {
234    /// Resolves the exact generation published by one prior workspace operation.
235    ///
236    /// This is the recovery boundary for adapters that persisted an
237    /// idempotency key before publication but lost the successful result.
238    pub async fn operation_generation(
239        &self,
240        idempotency_key: IdempotencyKey,
241    ) -> Result<Option<Generation<A, O>>, WorkspaceError> {
242        self.volume
243            .fs
244            .workspace_operation_generation(&self.volume, idempotency_key.operation_id())
245            .await
246            .map(|generation| {
247                generation.map(|id| Generation {
248                    workspace: self.clone(),
249                    id,
250                })
251            })
252    }
253
254    /// Opens an authenticated checkout using the requested generation and mode.
255    ///
256    /// # Errors
257    ///
258    /// Rejects unsupported mode combinations, unavailable generations, and
259    /// authority or immutable-storage failures.
260    pub async fn checkout(
261        &self,
262        selector: GenerationSelector,
263        mode: CheckoutMode,
264    ) -> Result<Checkout<A, O>, WorkspaceError> {
265        self.engine_checkout(selector, mode).await
266    }
267
268    /// Returns the attached native source, when this handle was created by
269    /// [`crate::Fs::attach_directory`].
270    #[cfg(all(feature = "native-watch", not(target_arch = "wasm32")))]
271    #[must_use]
272    pub fn source(&self) -> Option<&crate::Source<A, O>> {
273        self.source.as_ref()
274    }
275
276    /// Captures all remaining source state and returns an independent exact
277    /// generation. Workspaces without a source simply return their head.
278    ///
279    /// # Errors
280    ///
281    /// Returns source reconciliation or generation authentication failures.
282    #[cfg(all(feature = "native-watch", not(target_arch = "wasm32")))]
283    pub async fn seal(&self) -> Result<Generation<A, O>, crate::SourceError> {
284        match &self.source {
285            Some(source) => source.seal().await,
286            None => self
287                .head()
288                .await
289                .map_err(|error| crate::SourceError::Engine(error.to_string())),
290        }
291    }
292    /// Resolves and pins the current immutable generation for this operation.
293    ///
294    /// # Errors
295    ///
296    /// Returns a typed workspace failure when the head cannot be authenticated.
297    pub async fn head(&self) -> Result<Generation<A, O>, WorkspaceError> {
298        self.head_measured(
299            crate::WorkBudget::UNBOUNDED,
300            &crate::CancellationToken::new(),
301        )
302        .await
303        .map(|receipt| receipt.value)
304    }
305
306    pub(crate) async fn head_measured(
307        &self,
308        budget: crate::WorkBudget,
309        cancellation: &crate::CancellationToken,
310    ) -> Result<crate::OperationReceipt<Generation<A, O>>, WorkspaceError> {
311        let receipt = self
312            .volume
313            .checkout(
314                GenerationSelector::Head,
315                CheckoutMode::read_only_pinned(),
316                budget,
317                cancellation,
318            )
319            .await
320            .map_err(|failure| WorkspaceError::from(failure.error))?;
321        Ok(crate::OperationReceipt {
322            value: Generation {
323                workspace: self.clone(),
324                id: receipt.value.generation_id(),
325            },
326            work: receipt.work,
327        })
328    }
329
330    /// Reopens and authenticates one exact immutable generation belonging to
331    /// this workspace. This is the stateless transport/restart counterpart to
332    /// retaining a live [`Generation`] handle.
333    ///
334    /// # Errors
335    ///
336    /// Rejects absent, foreign, corrupt, or unauthenticated generation state.
337    pub async fn generation(
338        &self,
339        generation_id: GenerationId,
340    ) -> Result<Generation<A, O>, WorkspaceError> {
341        self.generation_measured(
342            generation_id,
343            crate::WorkBudget::UNBOUNDED,
344            &crate::CancellationToken::new(),
345        )
346        .await
347        .map(|receipt| receipt.value)
348    }
349
350    pub(crate) async fn generation_measured(
351        &self,
352        generation_id: GenerationId,
353        budget: crate::WorkBudget,
354        cancellation: &crate::CancellationToken,
355    ) -> Result<crate::OperationReceipt<Generation<A, O>>, WorkspaceError> {
356        let checkout = self
357            .engine_checkout_measured(
358                GenerationSelector::Exact(generation_id),
359                CheckoutMode::read_only_pinned(),
360                budget,
361                cancellation,
362            )
363            .await?;
364        Ok(crate::OperationReceipt {
365            value: Generation {
366                workspace: self.clone(),
367                id: checkout.value.generation_id(),
368            },
369            work: checkout.work,
370        })
371    }
372
373    /// Atomically moves this workspace head to one of its own exact immutable
374    /// generations. The caller supplies the observed head, making restoration
375    /// a composable compare-and-swap rather than an unconditional reset.
376    ///
377    /// This primitive is also the recovery boundary for compatibility layers:
378    /// retrying the same key and inputs reports the already-restored generation,
379    /// while reusing a key for another generation fails closed.
380    pub async fn restore_generation(
381        &self,
382        generation: &Generation<A, O>,
383        if_current: GenerationId,
384        idempotency_key: IdempotencyKey,
385    ) -> Result<WorkspaceRestore<A, O>, WorkspaceError> {
386        self.restore_generation_with_permit(
387            generation,
388            if_current,
389            idempotency_key,
390            crate::PublicationPermit::Unrestricted,
391        )
392        .await
393    }
394
395    /// Restores one exact generation only while the supplied writer permit remains valid.
396    pub async fn restore_generation_with_permit(
397        &self,
398        generation: &Generation<A, O>,
399        if_current: GenerationId,
400        idempotency_key: IdempotencyKey,
401        permit: crate::PublicationPermit,
402    ) -> Result<WorkspaceRestore<A, O>, WorkspaceError> {
403        if generation.workspace.id != self.id {
404            return Err(WorkspaceError::ForeignGeneration);
405        }
406        let outcome = self
407            .volume
408            .fs
409            .restore_workspace_generation(
410                &self.volume,
411                generation.id,
412                if_current,
413                idempotency_key.operation_id(),
414                permit,
415            )
416            .await?;
417        let restored = |id| Generation {
418            workspace: self.clone(),
419            id,
420        };
421        Ok(match outcome {
422            WorkspaceRestoreOutcome::Restored(id) => WorkspaceRestore::Restored(restored(id)),
423            WorkspaceRestoreOutcome::AlreadyRestored(id) => {
424                WorkspaceRestore::AlreadyRestored(restored(id))
425            }
426            WorkspaceRestoreOutcome::Current(id) => WorkspaceRestore::Current(restored(id)),
427            WorkspaceRestoreOutcome::Stale(id) => WorkspaceRestore::Stale(restored(id)),
428            WorkspaceRestoreOutcome::Fenced => WorkspaceRestore::Fenced,
429            WorkspaceRestoreOutcome::IdempotencyConflict => WorkspaceRestore::IdempotencyConflict,
430        })
431    }
432
433    /// Atomically restores selected paths from an immutable compatible
434    /// generation while leaving every other live path untouched.
435    ///
436    /// This is the core primitive used by compatibility layers for checkout,
437    /// restore, and hard reset. Exact records are reused by reference, richer
438    /// metadata is retained, and live hard-link topology remains authoritative
439    /// for bindings outside `paths`.
440    pub async fn restore_paths_from(
441        &self,
442        source: &Generation<A, O>,
443        paths: &[String],
444        if_current: GenerationId,
445        idempotency_key: IdempotencyKey,
446    ) -> Result<TransactionCommit<A, O>, WorkspaceError> {
447        self.restore_paths_from_with_permit(
448            source,
449            paths,
450            if_current,
451            idempotency_key,
452            crate::PublicationPermit::Unrestricted,
453        )
454        .await
455    }
456
457    /// Restores selected paths only while the supplied writer permit remains valid.
458    pub async fn restore_paths_from_with_permit(
459        &self,
460        source: &Generation<A, O>,
461        paths: &[String],
462        if_current: GenerationId,
463        idempotency_key: IdempotencyKey,
464        permit: crate::PublicationPermit,
465    ) -> Result<TransactionCommit<A, O>, WorkspaceError> {
466        if !self.volume.fs.same_deployment(&source.workspace.volume.fs) {
467            return Err(WorkspaceError::ForeignGeneration);
468        }
469        let current = self.head().await?;
470        if current.id != if_current {
471            return Ok(TransactionCommit::Conflict { actual: current });
472        }
473        let mut source_checkout = source
474            .workspace
475            .engine_checkout(
476                GenerationSelector::Exact(source.id),
477                CheckoutMode::read_only_pinned(),
478            )
479            .await?;
480        let mut transaction = self.begin_transaction(idempotency_key).await?;
481        let config = self.volume.config;
482        let cancellation = crate::CancellationToken::new();
483        let parsed = paths
484            .iter()
485            .map(|path| {
486                let relative = path.trim_start_matches('/');
487                let absolute = format!("/{relative}");
488                customer_path(&absolute, config).map(|path| (relative, path))
489            })
490            .collect::<Result<Vec<_>, _>>()?;
491        let lookup_paths = parsed
492            .iter()
493            .map(|(_, path)| path.clone())
494            .collect::<Vec<_>>();
495        let source_records = source_checkout
496            .lookup_batch_no_follow(&lookup_paths, crate::WorkBudget::UNBOUNDED, &cancellation)
497            .await
498            .map_err(WorkspaceError::engine)?
499            .value
500            .entries
501            .into_iter()
502            .map(|entry| entry.record)
503            .collect::<Vec<_>>();
504        let missing_paths = parsed
505            .iter()
506            .zip(&source_records)
507            .filter(|(_, record)| record.is_none())
508            .map(|((_, path), _)| path.clone())
509            .collect::<Vec<_>>();
510        let current_records = if missing_paths.is_empty() {
511            Vec::new()
512        } else {
513            transaction
514                .checkout
515                .lookup_batch_no_follow(&missing_paths, crate::WorkBudget::UNBOUNDED, &cancellation)
516                .await
517                .map_err(WorkspaceError::engine)?
518                .value
519                .entries
520                .into_iter()
521                .map(|entry| entry.record)
522                .collect()
523        };
524        let mut current_records = current_records.into_iter();
525        let mut operations = Vec::with_capacity(paths.len());
526        for ((relative, path), source_record) in parsed.into_iter().zip(source_records) {
527            if let Some(record) = source_record {
528                if record.kind != FileKind::Directory
529                    && let Some((parent, _)) = relative.rsplit_once('/')
530                {
531                    transaction.create_dir_all(&format!("/{parent}")).await?;
532                }
533                operations.push(crate::kernel::Mutation::Restore { path, record });
534            } else {
535                let current_record = current_records
536                    .next()
537                    .ok_or_else(|| WorkspaceError::engine("missing batch lookup result"))?;
538                if let Some(record) = current_record {
539                    operations.push(crate::kernel::Mutation::Remove {
540                        path,
541                        expected_file_id: MetadataField::Value(record.file_id),
542                    });
543                }
544            }
545        }
546        if !operations.is_empty() {
547            transaction
548                .checkout
549                .mutate(operations, crate::WorkBudget::UNBOUNDED, &cancellation)
550                .await
551                .map_err(WorkspaceError::engine)?;
552        }
553        transaction.commit_with_permit(permit).await
554    }
555
556    /// Applies only the paths changed between `base` and `source`, rejecting
557    /// paths independently changed in the live workspace. This is a bounded,
558    /// exact three-way patch primitive for cherry-pick, revert, and similar
559    /// compatibility operations.
560    #[allow(
561        clippy::too_many_lines,
562        reason = "one atomic path application keeps conflict classification and publication together"
563    )]
564    pub async fn apply_paths_from(
565        &self,
566        base: Option<&Generation<A, O>>,
567        source: Option<&Generation<A, O>>,
568        paths: &[String],
569        if_current: GenerationId,
570        idempotency_key: IdempotencyKey,
571    ) -> Result<WorkspacePathApply<A, O>, WorkspaceError> {
572        self.apply_paths_from_with_permit(
573            base,
574            source,
575            paths,
576            if_current,
577            idempotency_key,
578            crate::PublicationPermit::Unrestricted,
579        )
580        .await
581    }
582
583    /// Applies an exact three-way path delta under one writer permit.
584    #[allow(
585        clippy::too_many_lines,
586        reason = "one transaction keeps path planning, typed conflicts, and fenced publication atomic"
587    )]
588    pub async fn apply_paths_from_with_permit(
589        &self,
590        base: Option<&Generation<A, O>>,
591        source: Option<&Generation<A, O>>,
592        paths: &[String],
593        if_current: GenerationId,
594        idempotency_key: IdempotencyKey,
595        permit: crate::PublicationPermit,
596    ) -> Result<WorkspacePathApply<A, O>, WorkspaceError> {
597        if base.is_some_and(|base| !self.volume.fs.same_deployment(&base.workspace.volume.fs))
598            || source
599                .is_some_and(|source| !self.volume.fs.same_deployment(&source.workspace.volume.fs))
600        {
601            return Err(WorkspaceError::ForeignGeneration);
602        }
603        let current = self.head().await?;
604        if current.id != if_current {
605            return Ok(WorkspacePathApply::Stale(current));
606        }
607        let mut base_checkout = match base {
608            Some(base) => Some(
609                base.workspace
610                    .engine_checkout(
611                        GenerationSelector::Exact(base.id),
612                        CheckoutMode::read_only_pinned(),
613                    )
614                    .await?,
615            ),
616            None => None,
617        };
618        let mut source_checkout = match source {
619            Some(source) => Some(
620                source
621                    .workspace
622                    .engine_checkout(
623                        GenerationSelector::Exact(source.id),
624                        CheckoutMode::read_only_pinned(),
625                    )
626                    .await?,
627            ),
628            None => None,
629        };
630        let mut transaction = self.begin_transaction(idempotency_key).await?;
631        let config = self.volume.config;
632        let cancellation = crate::CancellationToken::new();
633        let parsed = paths
634            .iter()
635            .map(|path| {
636                let relative = path.trim_start_matches('/');
637                let absolute = format!("/{relative}");
638                customer_path(&absolute, config).map(|path| (relative, path))
639            })
640            .collect::<Result<Vec<_>, _>>()?;
641        let lookup_paths = parsed
642            .iter()
643            .map(|(_, path)| path.clone())
644            .collect::<Vec<_>>();
645        let base_records = match &mut base_checkout {
646            Some(checkout) => checkout
647                .lookup_batch_no_follow(&lookup_paths, crate::WorkBudget::UNBOUNDED, &cancellation)
648                .await
649                .map_err(WorkspaceError::engine)?
650                .value
651                .entries
652                .into_iter()
653                .map(|entry| entry.record)
654                .collect(),
655            None => vec![None; parsed.len()],
656        };
657        let source_records = match &mut source_checkout {
658            Some(checkout) => checkout
659                .lookup_batch_no_follow(&lookup_paths, crate::WorkBudget::UNBOUNDED, &cancellation)
660                .await
661                .map_err(WorkspaceError::engine)?
662                .value
663                .entries
664                .into_iter()
665                .map(|entry| entry.record)
666                .collect(),
667            None => vec![None; parsed.len()],
668        };
669        let changed_paths = parsed
670            .iter()
671            .zip(base_records.iter().zip(&source_records))
672            .filter(|(_, (base, source))| base != source)
673            .map(|((_, path), _)| path.clone())
674            .collect::<Vec<_>>();
675        let current_records = if changed_paths.is_empty() {
676            Vec::new()
677        } else {
678            transaction
679                .checkout
680                .lookup_batch_no_follow(&changed_paths, crate::WorkBudget::UNBOUNDED, &cancellation)
681                .await
682                .map_err(WorkspaceError::engine)?
683                .value
684                .entries
685                .into_iter()
686                .map(|entry| entry.record)
687                .collect()
688        };
689        let mut current_records = current_records.into_iter();
690        let mut operations = Vec::with_capacity(paths.len());
691        let mut conflicts = Vec::new();
692        for (((relative, path), base_record), source_record) in
693            parsed.into_iter().zip(base_records).zip(source_records)
694        {
695            if base_record == source_record {
696                continue;
697            }
698            let current_record = current_records
699                .next()
700                .ok_or_else(|| WorkspaceError::engine("missing batch lookup result"))?;
701            if current_record == source_record {
702                continue;
703            }
704            if current_record != base_record {
705                conflicts.push(WorkspacePathConflict {
706                    path: relative.to_owned(),
707                    kind: path_conflict_kind(base_record, current_record, source_record),
708                });
709                continue;
710            }
711            match source_record {
712                Some(record) => {
713                    if record.kind != FileKind::Directory
714                        && let Some((parent, _)) = relative.rsplit_once('/')
715                    {
716                        transaction.create_dir_all(&format!("/{parent}")).await?;
717                    }
718                    operations.push(crate::kernel::Mutation::Restore { path, record });
719                }
720                None => {
721                    if let Some(record) = current_record {
722                        operations.push(crate::kernel::Mutation::Remove {
723                            path,
724                            expected_file_id: MetadataField::Value(record.file_id),
725                        });
726                    }
727                }
728            }
729        }
730        if !conflicts.is_empty() {
731            return Ok(WorkspacePathApply::Conflicted(conflicts));
732        }
733        if operations.is_empty() {
734            return Ok(WorkspacePathApply::NoChanges(current));
735        }
736        transaction
737            .checkout
738            .mutate(operations, crate::WorkBudget::UNBOUNDED, &cancellation)
739            .await
740            .map_err(WorkspaceError::engine)?;
741        Ok(match transaction.commit_with_permit(permit).await? {
742            TransactionCommit::Committed(generation) => WorkspacePathApply::Applied(generation),
743            TransactionCommit::AlreadyCommitted(generation) => {
744                WorkspacePathApply::AlreadyApplied(generation)
745            }
746            TransactionCommit::Conflict { actual } => WorkspacePathApply::Stale(actual),
747            TransactionCommit::Fenced => WorkspacePathApply::Fenced,
748            TransactionCommit::IdempotencyConflict => WorkspacePathApply::IdempotencyConflict,
749        })
750    }
751
752    /// Returns the current complete immutable workspace state after all prior
753    /// SDK operations on this handle have reached their publication boundary.
754    ///
755    /// # Errors
756    ///
757    /// Returns an authority or authentication failure when the current head
758    /// cannot be resolved exactly.
759    pub async fn sync(&self) -> Result<WorkspaceSync<A, O>, WorkspaceError> {
760        Ok(WorkspaceSync {
761            generation: self.head().await?,
762        })
763    }
764
765    /// Creates an independent named workspace at one exact generation without
766    /// copying unchanged namespace pages or file bodies.
767    ///
768    /// # Errors
769    ///
770    /// Rejects invalid destination names, foreign generations, incompatible
771    /// existing destinations, or storage/publication failures.
772    pub async fn fork(
773        &self,
774        destination: impl AsRef<str>,
775        options: ForkOptions<A, O>,
776    ) -> Result<Self, WorkspaceError> {
777        if options.generation.workspace.id != self.id {
778            return Err(WorkspaceError::ForeignGeneration);
779        }
780        self.fork_measured(
781            destination,
782            options,
783            crate::WorkBudget::UNBOUNDED,
784            &crate::CancellationToken::new(),
785        )
786        .await
787        .map(|receipt| receipt.value)
788    }
789
790    pub(crate) async fn fork_measured(
791        &self,
792        destination: impl AsRef<str>,
793        options: ForkOptions<A, O>,
794        budget: crate::WorkBudget,
795        cancellation: &crate::CancellationToken,
796    ) -> Result<crate::OperationReceipt<Self>, WorkspaceError> {
797        if options.generation.workspace.id != self.id {
798            return Err(WorkspaceError::ForeignGeneration);
799        }
800        let destination = WorkspaceName::new(destination)?;
801        Box::pin(self.volume.fs.fork_workspace_measured(
802            destination,
803            &options.generation,
804            options.idempotency_key,
805            budget,
806            cancellation,
807        ))
808        .await
809    }
810
811    /// Opens one sparse atomic transaction against the current generation.
812    /// No mutation is visible outside this transaction before [`Transaction::commit`].
813    ///
814    /// # Errors
815    ///
816    /// Returns an authentication, authority, storage, or workspace-state failure.
817    pub async fn begin_transaction(
818        &self,
819        idempotency_key: IdempotencyKey,
820    ) -> Result<Transaction<A, O>, WorkspaceError> {
821        self.begin_transaction_measured(
822            idempotency_key,
823            crate::WorkBudget::UNBOUNDED,
824            &crate::CancellationToken::new(),
825        )
826        .await
827        .map(|receipt| receipt.value)
828    }
829
830    pub(crate) async fn begin_transaction_measured(
831        &self,
832        idempotency_key: IdempotencyKey,
833        budget: crate::WorkBudget,
834        cancellation: &crate::CancellationToken,
835    ) -> Result<crate::OperationReceipt<Transaction<A, O>>, WorkspaceError> {
836        let head = self.head_measured(budget, cancellation).await?;
837        let transaction = self
838            .open_transaction_at_measured(
839                &head.value,
840                idempotency_key,
841                false,
842                head.work
843                    .remaining(budget)
844                    .map_err(WorkspaceError::engine)?,
845                cancellation,
846            )
847            .await?;
848        Ok(crate::OperationReceipt {
849            value: transaction.value,
850            work: head
851                .work
852                .checked_add(transaction.work)
853                .map_err(WorkspaceError::engine)?,
854        })
855    }
856
857    /// Opens one sparse atomic transaction against an exact immutable generation.
858    ///
859    /// This is the stateless transport boundary: a remote caller can retain its
860    /// base generation, submit sparse mutations later, and receive the same
861    /// observation-safe commit or rebase result as an embedded caller. The
862    /// generation must belong to this workspace.
863    ///
864    /// # Errors
865    ///
866    /// Returns [`WorkspaceError::ForeignGeneration`] for another workspace, or
867    /// an authentication, authority, storage, or workspace-state failure.
868    pub async fn begin_transaction_at(
869        &self,
870        generation: &Generation<A, O>,
871        idempotency_key: IdempotencyKey,
872    ) -> Result<Transaction<A, O>, WorkspaceError> {
873        if generation.workspace.id != self.id {
874            return Err(WorkspaceError::ForeignGeneration);
875        }
876        let requires_rebase = self.head().await?.id != generation.id;
877        self.open_transaction_at(generation, idempotency_key, requires_rebase)
878            .await
879    }
880
881    pub(crate) async fn begin_pinned_transaction_measured(
882        &self,
883        generation: &Generation<A, O>,
884        idempotency_key: IdempotencyKey,
885        budget: crate::WorkBudget,
886        cancellation: &crate::CancellationToken,
887    ) -> Result<crate::OperationReceipt<Transaction<A, O>>, WorkspaceError> {
888        if generation.workspace.id != self.id {
889            return Err(WorkspaceError::ForeignGeneration);
890        }
891        self.open_transaction_at_measured(generation, idempotency_key, false, budget, cancellation)
892            .await
893    }
894
895    async fn open_transaction_at(
896        &self,
897        generation: &Generation<A, O>,
898        idempotency_key: IdempotencyKey,
899        requires_rebase: bool,
900    ) -> Result<Transaction<A, O>, WorkspaceError> {
901        self.open_transaction_at_measured(
902            generation,
903            idempotency_key,
904            requires_rebase,
905            crate::WorkBudget::UNBOUNDED,
906            &crate::CancellationToken::new(),
907        )
908        .await
909        .map(|receipt| receipt.value)
910    }
911
912    async fn open_transaction_at_measured(
913        &self,
914        generation: &Generation<A, O>,
915        idempotency_key: IdempotencyKey,
916        requires_rebase: bool,
917        budget: crate::WorkBudget,
918        cancellation: &crate::CancellationToken,
919    ) -> Result<crate::OperationReceipt<Transaction<A, O>>, WorkspaceError> {
920        let receipt = self
921            .engine_checkout_measured(
922                GenerationSelector::Exact(generation.id),
923                CheckoutMode::tracking_transaction(),
924                budget,
925                cancellation,
926            )
927            .await?;
928        let mut checkout = receipt.value;
929        checkout.bind_authored_operation(idempotency_key.operation_id());
930        Ok(crate::OperationReceipt {
931            value: Transaction {
932                checkout,
933                workspace: self.clone(),
934                idempotency_key,
935                requires_rebase,
936            },
937            work: receipt.work,
938        })
939    }
940
941    /// Retains the current generation under one permanent human-readable label.
942    /// Repeating the same label and generation is idempotent; binding the label
943    /// to another generation is rejected.
944    ///
945    /// # Errors
946    ///
947    /// Rejects invalid labels, generation authentication failures, or an
948    /// existing label bound to different state.
949    pub async fn checkpoint(
950        &self,
951        label: impl AsRef<str>,
952    ) -> Result<Checkpoint<A, O>, WorkspaceError> {
953        let label = WorkspaceName::new(label)?;
954        let generation = self.head().await?;
955        self.volume
956            .fs
957            .retain_workspace_generation(
958                &self.volume,
959                generation.id,
960                crate::kernel::RetentionKind::Checkpoint,
961                label.as_str().to_owned(),
962            )
963            .await?;
964        Ok(Checkpoint { label, generation })
965    }
966
967    /// Durably pins the current immutable generation under an opaque customer
968    /// identity. Repeating the same identity at the same generation is exact
969    /// and binding it to different state fails closed.
970    ///
971    /// # Errors
972    ///
973    /// Rejects invalid identities, generation authentication failures, or an
974    /// existing identity bound to different state.
975    pub async fn pin(
976        &self,
977        identity: impl AsRef<str>,
978    ) -> Result<GenerationPin<A, O>, WorkspaceError> {
979        self.head().await?.pin(identity).await
980    }
981
982    /// Terminally deletes this workspace head while leaving independently
983    /// retained checkpoints, pins, and forks valid.
984    ///
985    /// # Errors
986    ///
987    /// Returns a storage or authority failure when deletion cannot be durably
988    /// resolved. Semantic races are returned as typed outcomes.
989    pub async fn delete(
990        &self,
991        idempotency_key: IdempotencyKey,
992    ) -> Result<WorkspaceDelete, WorkspaceError> {
993        self.volume
994            .fs
995            .delete_workspace_volume(&self.volume, idempotency_key.operation_id())
996            .await
997    }
998
999    /// Computes one immutable bounded semantic change set between two exact
1000    /// generations of this workspace.
1001    ///
1002    /// # Errors
1003    ///
1004    /// Rejects foreign endpoints, zero bounds, malformed state, or a truncated
1005    /// authenticated diff frontier.
1006    pub async fn diff(
1007        &self,
1008        from: &Generation<A, O>,
1009        to: &Generation<A, O>,
1010        maximum_changes: u32,
1011    ) -> Result<ChangeSet<A, O>, WorkspaceError> {
1012        if from.workspace.id != self.id || to.workspace.id != self.id {
1013            return Err(WorkspaceError::ForeignGeneration);
1014        }
1015        let receipt = self
1016            .volume
1017            .diff_generations(
1018                from.id,
1019                to.id,
1020                maximum_changes,
1021                crate::WorkBudget::UNBOUNDED,
1022                &crate::CancellationToken::new(),
1023            )
1024            .await
1025            .map_err(WorkspaceError::engine)?;
1026        if receipt.value.truncated {
1027            return Err(WorkspaceError::JoinLimit);
1028        }
1029        Ok(ChangeSet {
1030            from: from.clone(),
1031            to: to.clone(),
1032            changes: receipt.value,
1033            work: receipt.work,
1034        })
1035    }
1036
1037    /// Begins a side-effect-free join description from this workspace into a
1038    /// target workspace in the same filesystem deployment.
1039    #[must_use]
1040    pub fn join_into(&self, target: &Workspace<A, O>) -> JoinBuilder<A, O> {
1041        JoinBuilder {
1042            source: self.clone(),
1043            target: target.clone(),
1044            history: JoinHistory::Merge,
1045            maximum_generations: 4_096,
1046            maximum_changes: self.volume.config().limits.maximum_paths_per_batch,
1047            maximum_conflicts: 1_024,
1048        }
1049    }
1050
1051    /// Advances this fork to its source workspace's current generation while
1052    /// preserving independently committed local changes.
1053    ///
1054    /// The source is recovered from authenticated immutable ancestry; no
1055    /// mutable fork catalog is required. Planning and merge are side-effect
1056    /// free, conflicts leave this workspace unchanged, and success publishes
1057    /// through one idempotent head CAS.
1058    ///
1059    /// # Errors
1060    ///
1061    /// Rejects a workspace that is not a fork, incompatible source semantics,
1062    /// exhausted lineage/conflict bounds, malformed state, or backend failure.
1063    pub async fn live_rebase(
1064        &self,
1065        idempotency_key: IdempotencyKey,
1066        maximum_generations: u32,
1067        maximum_changes: u32,
1068        maximum_conflicts: u32,
1069    ) -> Result<WorkspaceRebase<A, O>, WorkspaceError> {
1070        let outcome = self
1071            .volume
1072            .fs
1073            .live_rebase_workspace(crate::facade::WorkspaceRebaseRequest {
1074                target: &self.volume,
1075                operation_id: idempotency_key.operation_id(),
1076                maximum_generations,
1077                maximum_changes,
1078                maximum_conflicts,
1079            })
1080            .await?;
1081        let generation = |id| Generation {
1082            workspace: self.clone(),
1083            id,
1084        };
1085        Ok(match outcome {
1086            crate::facade::WorkspaceJoinOutcome::Applied(id) => {
1087                WorkspaceRebase::Rebased(generation(id))
1088            }
1089            crate::facade::WorkspaceJoinOutcome::AlreadyApplied(id) => {
1090                WorkspaceRebase::AlreadyRebased(generation(id))
1091            }
1092            crate::facade::WorkspaceJoinOutcome::NoChanges(id) => {
1093                WorkspaceRebase::Current(generation(id))
1094            }
1095            crate::facade::WorkspaceJoinOutcome::Stale(id) => {
1096                WorkspaceRebase::Stale(generation(id))
1097            }
1098            crate::facade::WorkspaceJoinOutcome::Conflicted(conflicts, truncated) => {
1099                WorkspaceRebase::Conflicted {
1100                    conflicts,
1101                    truncated,
1102                }
1103            }
1104            crate::facade::WorkspaceJoinOutcome::Fenced => WorkspaceRebase::Fenced,
1105            crate::facade::WorkspaceJoinOutcome::IdempotencyConflict => {
1106                WorkspaceRebase::IdempotencyConflict
1107            }
1108        })
1109    }
1110
1111    /// Reads at most `maximum_bytes` from one complete file at the current
1112    /// immutable head.
1113    ///
1114    /// # Errors
1115    ///
1116    /// Rejects invalid/absent/non-file paths, files above the supplied bound,
1117    /// and authenticated backend failures.
1118    pub async fn read(&self, path: &str, maximum_bytes: u64) -> Result<Bytes, WorkspaceError> {
1119        read_generation(self, GenerationSelector::Head, path, maximum_bytes).await
1120    }
1121
1122    /// Reads one exact regular-file range without materializing unrelated bytes.
1123    ///
1124    /// # Errors
1125    ///
1126    /// Rejects invalid/absent/non-file paths, invalid ranges, and authenticated
1127    /// backend failures.
1128    pub async fn read_range(
1129        &self,
1130        path: &str,
1131        offset: u64,
1132        length: u64,
1133    ) -> Result<Bytes, WorkspaceError> {
1134        read_generation_range(self, GenerationSelector::Head, path, offset, length).await
1135    }
1136
1137    /// Returns complete path identity, kind, size, link count, and metadata.
1138    ///
1139    /// # Errors
1140    ///
1141    /// Rejects invalid or absent paths and authenticated backend failures.
1142    pub async fn stat(&self, path: &str) -> Result<WorkspaceStat, WorkspaceError> {
1143        stat_generation(self, GenerationSelector::Head, path).await
1144    }
1145
1146    pub(crate) async fn stat_optional_measured(
1147        &self,
1148        path: &str,
1149        budget: crate::WorkBudget,
1150        cancellation: &crate::CancellationToken,
1151    ) -> Result<crate::OperationReceipt<Option<WorkspaceStat>>, WorkspaceError> {
1152        stat_generation_optional_measured(
1153            self,
1154            GenerationSelector::Head,
1155            path,
1156            budget,
1157            cancellation,
1158        )
1159        .await
1160    }
1161
1162    pub(crate) async fn record_by_id(
1163        &self,
1164        file_id: FileId,
1165    ) -> Result<crate::kernel::FileRecord, WorkspaceError> {
1166        let mut checkout = self
1167            .engine_checkout(GenerationSelector::Head, CheckoutMode::read_only_pinned())
1168            .await?;
1169        checkout
1170            .read_file_record_by_id(
1171                file_id,
1172                crate::WorkBudget::UNBOUNDED,
1173                &crate::CancellationToken::new(),
1174            )
1175            .await
1176            .map(|receipt| receipt.value)
1177            .map_err(WorkspaceError::engine)
1178    }
1179
1180    pub(crate) fn detached_record(
1181        &self,
1182        record: crate::kernel::FileRecord,
1183    ) -> crate::DetachedFile<A, O> {
1184        crate::DetachedFile::from_record(self.volume.clone(), record)
1185    }
1186
1187    /// Returns one authenticated bounded directory page.
1188    ///
1189    /// # Errors
1190    ///
1191    /// Rejects invalid/absent/non-directory paths, invalid cursors or bounds,
1192    /// and authenticated backend failures.
1193    pub async fn list_directory(
1194        &self,
1195        path: &str,
1196        after: Option<&LogicalName>,
1197        maximum_entries: u32,
1198    ) -> Result<WorkspaceDirectoryPage, WorkspaceError> {
1199        list_generation_directory(self, GenerationSelector::Head, path, after, maximum_entries)
1200            .await
1201    }
1202
1203    pub(crate) async fn list_directory_measured(
1204        &self,
1205        path: &str,
1206        after: Option<&LogicalName>,
1207        maximum_entries: u32,
1208        budget: crate::WorkBudget,
1209        cancellation: &crate::CancellationToken,
1210    ) -> Result<crate::OperationReceipt<WorkspaceDirectoryPage>, WorkspaceError> {
1211        list_generation_directory_measured(
1212            self,
1213            GenerationSelector::Head,
1214            path,
1215            after,
1216            maximum_entries,
1217            budget,
1218            cancellation,
1219        )
1220        .await
1221    }
1222
1223    /// Reads exact opaque symbolic-link target bytes without following it.
1224    ///
1225    /// # Errors
1226    ///
1227    /// Rejects invalid/absent/non-symbolic-link paths and authenticated backend
1228    /// failures.
1229    pub async fn read_symbolic_link(&self, path: &str) -> Result<Bytes, WorkspaceError> {
1230        read_generation_symbolic_link(self, GenerationSelector::Head, path).await
1231    }
1232
1233    /// Plans sparse logical spans without reading content bodies.
1234    ///
1235    /// # Errors
1236    ///
1237    /// Rejects invalid/absent/non-file paths, invalid ranges or bounds, and
1238    /// authenticated backend failures.
1239    pub async fn plan_extents(
1240        &self,
1241        path: &str,
1242        offset: u64,
1243        length: u64,
1244        maximum_spans: u32,
1245    ) -> Result<WorkspaceExtentPlan, WorkspaceError> {
1246        plan_generation_extents(
1247            self,
1248            GenerationSelector::Head,
1249            path,
1250            offset,
1251            length,
1252            maximum_spans,
1253        )
1254        .await
1255    }
1256
1257    /// Creates or replaces one complete UTF-8 file in a single atomic
1258    /// generation publication.
1259    ///
1260    /// # Errors
1261    ///
1262    /// Rejects invalid paths, unsupported destinations, stale publication, or
1263    /// authenticated storage failures.
1264    pub async fn write_text(
1265        &self,
1266        path: &str,
1267        text: &str,
1268    ) -> Result<TransactionCommit<A, O>, WorkspaceError> {
1269        self.write(path, Bytes::copy_from_slice(text.as_bytes()))
1270            .await
1271    }
1272
1273    /// Creates or replaces one complete file in a single atomic generation.
1274    ///
1275    /// # Errors
1276    ///
1277    /// Rejects invalid paths, unsupported destinations, stale publication,
1278    /// size limits, or authenticated storage failures.
1279    pub async fn write(
1280        &self,
1281        path: &str,
1282        bytes: Bytes,
1283    ) -> Result<TransactionCommit<A, O>, WorkspaceError> {
1284        let mut transaction = self.begin_transaction(IdempotencyKey::new()).await?;
1285        transaction.write(path, bytes).await?;
1286        transaction.commit().await
1287    }
1288
1289    /// Removes one existing path in a single atomic generation publication.
1290    ///
1291    /// # Errors
1292    ///
1293    /// Rejects invalid or absent paths and bounded engine failures.
1294    pub async fn remove(&self, path: &str) -> Result<TransactionCommit<A, O>, WorkspaceError> {
1295        let mut transaction = self.begin_transaction(IdempotencyKey::new()).await?;
1296        transaction.remove(path).await?;
1297        transaction.commit().await
1298    }
1299
1300    /// Copies one complete regular file within this workspace using immutable
1301    /// extent references instead of reading or copying file bodies.
1302    ///
1303    /// # Errors
1304    ///
1305    /// Rejects invalid paths, non-regular sources or destinations, and bounded
1306    /// publication failures.
1307    pub async fn copy(
1308        &self,
1309        source: &str,
1310        destination: &str,
1311    ) -> Result<TransactionCommit<A, O>, WorkspaceError> {
1312        let mut transaction = self.begin_transaction(IdempotencyKey::new()).await?;
1313        transaction.copy(source, destination).await?;
1314        transaction.commit().await
1315    }
1316
1317    async fn read_selected(
1318        &self,
1319        selector: GenerationSelector,
1320        path: &str,
1321        maximum_bytes: u64,
1322    ) -> Result<Bytes, WorkspaceError> {
1323        let mut checkout = self
1324            .engine_checkout(selector, CheckoutMode::read_only_pinned())
1325            .await?;
1326        let path = customer_path(path, checkout.volume_config())?;
1327        let lookup = checkout
1328            .lookup_no_follow(
1329                &path,
1330                crate::WorkBudget::UNBOUNDED,
1331                &crate::CancellationToken::new(),
1332            )
1333            .await
1334            .map_err(WorkspaceError::engine)?
1335            .value
1336            .record
1337            .ok_or(WorkspaceError::NotFound)?;
1338        if lookup.kind != FileKind::Regular {
1339            return Err(WorkspaceError::NotRegularFile);
1340        }
1341        let logical_bytes = regular_file_bytes(lookup)?;
1342        if logical_bytes > maximum_bytes {
1343            return Err(WorkspaceError::ReadLimitExceeded);
1344        }
1345        checkout
1346            .read_file_range(
1347                &path,
1348                crate::ByteRange {
1349                    offset: 0,
1350                    length: logical_bytes,
1351                },
1352                crate::WorkBudget::UNBOUNDED,
1353                &crate::CancellationToken::new(),
1354            )
1355            .await
1356            .map(|receipt| receipt.value.bytes)
1357            .map_err(WorkspaceError::engine)
1358    }
1359
1360    pub(crate) async fn engine_checkout(
1361        &self,
1362        selector: GenerationSelector,
1363        mode: CheckoutMode,
1364    ) -> Result<Checkout<A, O>, WorkspaceError> {
1365        self.engine_checkout_measured(
1366            selector,
1367            mode,
1368            crate::WorkBudget::UNBOUNDED,
1369            &crate::CancellationToken::new(),
1370        )
1371        .await
1372        .map(|receipt| receipt.value)
1373    }
1374
1375    pub(crate) async fn engine_checkout_measured(
1376        &self,
1377        selector: GenerationSelector,
1378        mode: CheckoutMode,
1379        budget: crate::WorkBudget,
1380        cancellation: &crate::CancellationToken,
1381    ) -> Result<crate::OperationReceipt<Checkout<A, O>>, WorkspaceError> {
1382        self.volume
1383            .checkout(selector, mode, budget, cancellation)
1384            .await
1385            .map_err(|failure| WorkspaceError::from(failure.error))
1386    }
1387}
1388
1389async fn read_generation<A: AsyncAuthorityStore, O: AsyncObjectStore>(
1390    workspace: &Workspace<A, O>,
1391    selector: GenerationSelector,
1392    path: &str,
1393    maximum_bytes: u64,
1394) -> Result<Bytes, WorkspaceError> {
1395    workspace.read_selected(selector, path, maximum_bytes).await
1396}
1397
1398async fn read_generation_range<A: AsyncAuthorityStore, O: AsyncObjectStore>(
1399    workspace: &Workspace<A, O>,
1400    selector: GenerationSelector,
1401    path: &str,
1402    offset: u64,
1403    length: u64,
1404) -> Result<Bytes, WorkspaceError> {
1405    let mut checkout = workspace
1406        .engine_checkout(selector, CheckoutMode::read_only_pinned())
1407        .await?;
1408    let path = customer_path(path, checkout.volume_config())?;
1409    checkout
1410        .read_file_range(
1411            &path,
1412            crate::ByteRange { offset, length },
1413            crate::WorkBudget::UNBOUNDED,
1414            &crate::CancellationToken::new(),
1415        )
1416        .await
1417        .map(|receipt| receipt.value.bytes)
1418        .map_err(|failure| WorkspaceError::from(failure.error))
1419}
1420
1421async fn stat_generation<A: AsyncAuthorityStore, O: AsyncObjectStore>(
1422    workspace: &Workspace<A, O>,
1423    selector: GenerationSelector,
1424    path: &str,
1425) -> Result<WorkspaceStat, WorkspaceError> {
1426    stat_generation_measured(
1427        workspace,
1428        selector,
1429        path,
1430        crate::WorkBudget::UNBOUNDED,
1431        &crate::CancellationToken::new(),
1432    )
1433    .await
1434    .map(|receipt| receipt.value)
1435}
1436
1437async fn stat_generation_measured<A: AsyncAuthorityStore, O: AsyncObjectStore>(
1438    workspace: &Workspace<A, O>,
1439    selector: GenerationSelector,
1440    path: &str,
1441    budget: crate::WorkBudget,
1442    cancellation: &crate::CancellationToken,
1443) -> Result<crate::OperationReceipt<WorkspaceStat>, WorkspaceError> {
1444    let receipt =
1445        stat_generation_optional_measured(workspace, selector, path, budget, cancellation).await?;
1446    Ok(crate::OperationReceipt {
1447        value: receipt.value.ok_or(WorkspaceError::NotFound)?,
1448        work: receipt.work,
1449    })
1450}
1451
1452async fn stat_generation_optional_measured<A: AsyncAuthorityStore, O: AsyncObjectStore>(
1453    workspace: &Workspace<A, O>,
1454    selector: GenerationSelector,
1455    path: &str,
1456    budget: crate::WorkBudget,
1457    cancellation: &crate::CancellationToken,
1458) -> Result<crate::OperationReceipt<Option<WorkspaceStat>>, WorkspaceError> {
1459    let checkout = workspace
1460        .engine_checkout_measured(
1461            selector,
1462            CheckoutMode::read_only_pinned(),
1463            budget,
1464            cancellation,
1465        )
1466        .await?;
1467    let mut work = checkout.work;
1468    let mut checkout = checkout.value;
1469    let path = customer_path(path, checkout.volume_config())?;
1470    let lookup = checkout
1471        .lookup_no_follow_with_metadata(
1472            &path,
1473            work.remaining(budget).map_err(WorkspaceError::from)?,
1474            cancellation,
1475        )
1476        .await
1477        .map_err(|failure| WorkspaceError::from(failure.error))?;
1478    work = work
1479        .checked_add(lookup.work)
1480        .map_err(WorkspaceError::from)?;
1481    let Some(lookup) = lookup.value else {
1482        return Ok(crate::OperationReceipt { value: None, work });
1483    };
1484    let logical_bytes = match &lookup.record.payload {
1485        FilePayload::InlineRegular(bytes) => {
1486            Some(u64::try_from(bytes.as_bytes().len()).map_err(WorkspaceError::engine)?)
1487        }
1488        FilePayload::Regular { logical_bytes, .. }
1489        | FilePayload::SymbolicLink {
1490            target_bytes: logical_bytes,
1491            ..
1492        }
1493        | FilePayload::ReparsePoint {
1494            payload_bytes: logical_bytes,
1495            ..
1496        } => Some(*logical_bytes),
1497        FilePayload::Directory { .. } | FilePayload::Device { .. } | FilePayload::Empty => None,
1498    };
1499    Ok(crate::OperationReceipt {
1500        value: Some(WorkspaceStat {
1501            file_id: lookup.record.file_id,
1502            kind: lookup.record.kind,
1503            link_count: lookup.record.link_count,
1504            logical_bytes,
1505            metadata: WorkspaceMetadata::from_engine(lookup.metadata),
1506        }),
1507        work,
1508    })
1509}
1510
1511async fn list_generation_directory<A: AsyncAuthorityStore, O: AsyncObjectStore>(
1512    workspace: &Workspace<A, O>,
1513    selector: GenerationSelector,
1514    path: &str,
1515    after: Option<&LogicalName>,
1516    maximum_entries: u32,
1517) -> Result<WorkspaceDirectoryPage, WorkspaceError> {
1518    list_generation_directory_measured(
1519        workspace,
1520        selector,
1521        path,
1522        after,
1523        maximum_entries,
1524        crate::WorkBudget::UNBOUNDED,
1525        &crate::CancellationToken::new(),
1526    )
1527    .await
1528    .map(|receipt| receipt.value)
1529}
1530
1531#[allow(clippy::too_many_arguments)]
1532async fn list_generation_directory_measured<A: AsyncAuthorityStore, O: AsyncObjectStore>(
1533    workspace: &Workspace<A, O>,
1534    selector: GenerationSelector,
1535    path: &str,
1536    after: Option<&LogicalName>,
1537    maximum_entries: u32,
1538    budget: crate::WorkBudget,
1539    cancellation: &crate::CancellationToken,
1540) -> Result<crate::OperationReceipt<WorkspaceDirectoryPage>, WorkspaceError> {
1541    let checkout = workspace
1542        .engine_checkout_measured(
1543            selector,
1544            CheckoutMode::read_only_pinned(),
1545            budget,
1546            cancellation,
1547        )
1548        .await?;
1549    let prior = checkout.work;
1550    let mut checkout = checkout.value;
1551    let path = customer_path(path, checkout.volume_config())?;
1552    let page = checkout
1553        .list_directory(
1554            &path,
1555            after,
1556            maximum_entries,
1557            prior.remaining(budget).map_err(WorkspaceError::from)?,
1558            cancellation,
1559        )
1560        .await
1561        .map_err(|failure| WorkspaceError::from(failure.error))?;
1562    let work = prior.checked_add(page.work).map_err(WorkspaceError::from)?;
1563    Ok(crate::OperationReceipt {
1564        value: WorkspaceDirectoryPage {
1565            entries: page
1566                .value
1567                .entries
1568                .into_iter()
1569                .map(|entry| WorkspaceDirectoryEntry {
1570                    name: entry.name,
1571                    file_id: entry.file_id,
1572                    kind: entry.kind,
1573                })
1574                .collect(),
1575            has_more: page.value.has_more,
1576        },
1577        work,
1578    })
1579}
1580
1581async fn read_generation_symbolic_link<A: AsyncAuthorityStore, O: AsyncObjectStore>(
1582    workspace: &Workspace<A, O>,
1583    selector: GenerationSelector,
1584    path: &str,
1585) -> Result<Bytes, WorkspaceError> {
1586    let mut checkout = workspace
1587        .engine_checkout(selector, CheckoutMode::read_only_pinned())
1588        .await?;
1589    let path = customer_path(path, checkout.volume_config())?;
1590    checkout
1591        .read_symbolic_link(
1592            &path,
1593            crate::WorkBudget::UNBOUNDED,
1594            &crate::CancellationToken::new(),
1595        )
1596        .await
1597        .map(|receipt| receipt.value)
1598        .map_err(WorkspaceError::engine)
1599}
1600
1601async fn plan_generation_extents<A: AsyncAuthorityStore, O: AsyncObjectStore>(
1602    workspace: &Workspace<A, O>,
1603    selector: GenerationSelector,
1604    path: &str,
1605    offset: u64,
1606    length: u64,
1607    maximum_spans: u32,
1608) -> Result<WorkspaceExtentPlan, WorkspaceError> {
1609    let mut checkout = workspace
1610        .engine_checkout(selector, CheckoutMode::read_only_pinned())
1611        .await?;
1612    let path = customer_path(path, checkout.volume_config())?;
1613    let plan = checkout
1614        .plan_file_extents(
1615            &path,
1616            crate::ByteRange { offset, length },
1617            maximum_spans,
1618            crate::WorkBudget::UNBOUNDED,
1619            &crate::CancellationToken::new(),
1620        )
1621        .await
1622        .map_err(WorkspaceError::engine)?
1623        .value;
1624    let spans = match plan {
1625        Some(plan) => plan
1626            .spans
1627            .into_iter()
1628            .map(|span| WorkspaceExtentSpan {
1629                offset: span.offset,
1630                length: span.length,
1631                source_end: span.source_end,
1632                kind: match span.kind {
1633                    ExtentKind::Hole => WorkspaceExtentKind::Hole,
1634                    ExtentKind::AllocatedZero => WorkspaceExtentKind::AllocatedZero,
1635                    ExtentKind::Content { .. } => WorkspaceExtentKind::Content,
1636                },
1637            })
1638            .collect(),
1639        None if length == 0 => Vec::new(),
1640        None => vec![WorkspaceExtentSpan {
1641            offset,
1642            length,
1643            source_end: offset
1644                .checked_add(length)
1645                .ok_or_else(|| WorkspaceError::engine("file range overflows"))?,
1646            kind: WorkspaceExtentKind::Content,
1647        }],
1648    };
1649    Ok(WorkspaceExtentPlan { spans })
1650}
1651
1652/// One unpublished sparse atomic workspace transaction.
1653pub struct Transaction<A, O> {
1654    workspace: Workspace<A, O>,
1655    checkout: Checkout<A, O>,
1656    idempotency_key: IdempotencyKey,
1657    requires_rebase: bool,
1658}
1659
1660impl<A: AsyncAuthorityStore, O: AsyncObjectStore> Transaction<A, O> {
1661    /// Reuses the workspace transaction compiler against a private checkout
1662    /// candidate without publishing a second workspace head.
1663    #[cfg(feature = "native-mount")]
1664    pub(crate) fn for_checkout_candidate(
1665        workspace: &Workspace<A, O>,
1666        checkout: Checkout<A, O>,
1667    ) -> Self {
1668        Self {
1669            workspace: workspace.clone(),
1670            checkout,
1671            idempotency_key: IdempotencyKey::new(),
1672            requires_rebase: false,
1673        }
1674    }
1675
1676    #[cfg(feature = "native-mount")]
1677    pub(crate) fn into_checkout_candidate(self) -> Checkout<A, O> {
1678        self.checkout
1679    }
1680
1681    pub(crate) fn workspace_id(&self) -> WorkspaceId {
1682        self.workspace.id()
1683    }
1684
1685    pub(crate) async fn restore_record_measured(
1686        &mut self,
1687        path: &str,
1688        record: crate::kernel::FileRecord,
1689        budget: crate::WorkBudget,
1690        cancellation: &crate::CancellationToken,
1691    ) -> Result<crate::WorkCounters, WorkspaceError> {
1692        let path = customer_path(path, self.checkout.volume_config())?;
1693        self.apply_engine_measured(
1694            vec![crate::kernel::Mutation::Restore { path, record }],
1695            budget,
1696            cancellation,
1697        )
1698        .await
1699    }
1700
1701    pub(crate) async fn create_dir_all_measured(
1702        &mut self,
1703        path: &str,
1704        budget: crate::WorkBudget,
1705        cancellation: &crate::CancellationToken,
1706    ) -> Result<crate::WorkCounters, WorkspaceError> {
1707        let portable = PortablePath::parse(path, self.checkout.volume_config().limits)
1708            .map_err(WorkspaceError::path)?;
1709        let mut current = String::new();
1710        let mut work = crate::WorkCounters::default();
1711        for component in portable.components() {
1712            cancellation.check().map_err(WorkspaceError::from)?;
1713            current.push('/');
1714            current.push_str(component);
1715            let path = customer_path(&current, self.checkout.volume_config())?;
1716            let lookup = self
1717                .checkout
1718                .lookup_no_follow(
1719                    &path,
1720                    work.remaining(budget).map_err(WorkspaceError::from)?,
1721                    cancellation,
1722                )
1723                .await
1724                .map_err(|failure| WorkspaceError::from(failure.error))?;
1725            work = work
1726                .checked_add(lookup.work)
1727                .map_err(WorkspaceError::from)?;
1728            match lookup.value.record {
1729                Some(value) if value.kind == FileKind::Directory => {}
1730                Some(_) => return Err(WorkspaceError::NotDirectory),
1731                None => {
1732                    let applied = self
1733                        .apply_measured(
1734                            vec![AuthoredMutation::CreateDirectory {
1735                                path,
1736                                metadata: FileMetadata::default(),
1737                            }],
1738                            work.remaining(budget).map_err(WorkspaceError::from)?,
1739                            cancellation,
1740                        )
1741                        .await?;
1742                    work = work.checked_add(applied).map_err(WorkspaceError::from)?;
1743                }
1744            }
1745        }
1746        Ok(work)
1747    }
1748
1749    pub(crate) async fn apply_authored_measured(
1750        &mut self,
1751        operation: AuthoredMutation,
1752        budget: crate::WorkBudget,
1753        cancellation: &crate::CancellationToken,
1754    ) -> Result<crate::WorkCounters, WorkspaceError> {
1755        self.apply_measured(vec![operation], budget, cancellation)
1756            .await
1757    }
1758
1759    /// Admits a bounded ordered capture as one unpublished candidate update.
1760    pub(crate) async fn apply_authored_bulk_measured(
1761        &mut self,
1762        operations: Vec<AuthoredMutation>,
1763        budget: crate::WorkBudget,
1764        cancellation: &crate::CancellationToken,
1765    ) -> Result<crate::WorkCounters, WorkspaceError> {
1766        self.checkout
1767            .apply_authored_bulk_transaction(operations, budget, cancellation)
1768            .await
1769            .map(|receipt| receipt.work)
1770            .map_err(|failure| WorkspaceError::from(failure.error))
1771    }
1772
1773    pub(crate) async fn create_directory_measured(
1774        &mut self,
1775        path: &str,
1776        budget: crate::WorkBudget,
1777        cancellation: &crate::CancellationToken,
1778    ) -> Result<crate::WorkCounters, WorkspaceError> {
1779        let path = customer_path(path, self.checkout.volume_config())?;
1780        self.apply_authored_measured(
1781            AuthoredMutation::CreateDirectory {
1782                path,
1783                metadata: FileMetadata::default(),
1784            },
1785            budget,
1786            cancellation,
1787        )
1788        .await
1789    }
1790
1791    pub(crate) async fn create_file_measured(
1792        &mut self,
1793        path: &str,
1794        bytes: Bytes,
1795        budget: crate::WorkBudget,
1796        cancellation: &crate::CancellationToken,
1797    ) -> Result<crate::WorkCounters, WorkspaceError> {
1798        let path = customer_path(path, self.checkout.volume_config())?;
1799        self.apply_authored_measured(
1800            AuthoredMutation::CreateFile {
1801                path,
1802                bytes,
1803                metadata: FileMetadata::default(),
1804            },
1805            budget,
1806            cancellation,
1807        )
1808        .await
1809    }
1810
1811    pub(crate) async fn write_range_measured(
1812        &mut self,
1813        path: &str,
1814        offset: u64,
1815        bytes: Bytes,
1816        budget: crate::WorkBudget,
1817        cancellation: &crate::CancellationToken,
1818    ) -> Result<crate::WorkCounters, WorkspaceError> {
1819        let path = customer_path(path, self.checkout.volume_config())?;
1820        self.apply_authored_measured(
1821            AuthoredMutation::Write {
1822                path,
1823                offset,
1824                bytes,
1825            },
1826            budget,
1827            cancellation,
1828        )
1829        .await
1830    }
1831
1832    pub(crate) async fn create_symbolic_link_measured(
1833        &mut self,
1834        path: &str,
1835        target: Bytes,
1836        budget: crate::WorkBudget,
1837        cancellation: &crate::CancellationToken,
1838    ) -> Result<crate::WorkCounters, WorkspaceError> {
1839        let path = customer_path(path, self.checkout.volume_config())?;
1840        self.apply_authored_measured(
1841            AuthoredMutation::CreateSymbolicLink {
1842                path,
1843                target,
1844                metadata: FileMetadata::default(),
1845            },
1846            budget,
1847            cancellation,
1848        )
1849        .await
1850    }
1851
1852    pub(crate) async fn create_special_measured(
1853        &mut self,
1854        path: &str,
1855        kind: FileKind,
1856        device: Option<(u32, u32)>,
1857        budget: crate::WorkBudget,
1858        cancellation: &crate::CancellationToken,
1859    ) -> Result<crate::WorkCounters, WorkspaceError> {
1860        let path = customer_path(path, self.checkout.volume_config())?;
1861        let operation = match (kind, device) {
1862            (FileKind::Fifo | FileKind::Socket, None) => AuthoredMutation::CreateEmptySpecial {
1863                path,
1864                kind,
1865                metadata: FileMetadata::default(),
1866            },
1867            (FileKind::CharacterDevice | FileKind::BlockDevice, Some((major, minor))) => {
1868                AuthoredMutation::CreateDevice {
1869                    path,
1870                    kind,
1871                    major,
1872                    minor,
1873                    metadata: FileMetadata::default(),
1874                }
1875            }
1876            _ => return Err(WorkspaceError::IncompatibleWorkspace),
1877        };
1878        self.apply_authored_measured(operation, budget, cancellation)
1879            .await
1880    }
1881
1882    pub(crate) async fn set_metadata_measured(
1883        &mut self,
1884        path: &str,
1885        metadata: FileMetadata,
1886        budget: crate::WorkBudget,
1887        cancellation: &crate::CancellationToken,
1888    ) -> Result<crate::WorkCounters, WorkspaceError> {
1889        let path = customer_path(path, self.checkout.volume_config())?;
1890        self.apply_authored_measured(
1891            AuthoredMutation::SetMetadata { path, metadata },
1892            budget,
1893            cancellation,
1894        )
1895        .await
1896    }
1897
1898    pub(crate) async fn preserve_file_identity_measured(
1899        &mut self,
1900        path: &str,
1901        file_id: FileId,
1902        budget: crate::WorkBudget,
1903        cancellation: &crate::CancellationToken,
1904    ) -> Result<crate::WorkCounters, WorkspaceError> {
1905        let path = customer_path(path, self.checkout.volume_config())?;
1906        self.apply_authored_measured(
1907            AuthoredMutation::Reidentify { path, file_id },
1908            budget,
1909            cancellation,
1910        )
1911        .await
1912    }
1913
1914    pub(crate) async fn hard_link_measured(
1915        &mut self,
1916        source: &str,
1917        destination: &str,
1918        budget: crate::WorkBudget,
1919        cancellation: &crate::CancellationToken,
1920    ) -> Result<crate::WorkCounters, WorkspaceError> {
1921        let config = self.checkout.volume_config();
1922        self.apply_authored_measured(
1923            AuthoredMutation::HardLink {
1924                source: customer_path(source, config)?,
1925                destination: customer_path(destination, config)?,
1926            },
1927            budget,
1928            cancellation,
1929        )
1930        .await
1931    }
1932
1933    async fn apply_engine_measured(
1934        &mut self,
1935        operations: Vec<crate::kernel::Mutation>,
1936        budget: crate::WorkBudget,
1937        cancellation: &crate::CancellationToken,
1938    ) -> Result<crate::WorkCounters, WorkspaceError> {
1939        self.checkout
1940            .mutate(operations, budget, cancellation)
1941            .await
1942            .map(|receipt| receipt.work)
1943            .map_err(|failure| WorkspaceError::from(failure.error))
1944    }
1945
1946    /// Preserves a stable source identity for an existing non-directory object.
1947    ///
1948    /// Reusing an identity creates a hard-link alias only when kind, metadata,
1949    /// and payload match exactly; collisions fail closed.
1950    pub async fn preserve_file_identity(
1951        &mut self,
1952        path: &str,
1953        file_id: FileId,
1954    ) -> Result<(), WorkspaceError> {
1955        let path = customer_path(path, self.checkout.volume_config())?;
1956        self.apply(vec![AuthoredMutation::Reidentify { path, file_id }])
1957            .await
1958    }
1959
1960    /// Creates one new regular file and rejects an existing destination.
1961    ///
1962    /// # Errors
1963    ///
1964    /// Rejects invalid or existing paths, unsupported metadata, size limits,
1965    /// and authenticated engine failures.
1966    pub async fn create_file(
1967        &mut self,
1968        path: &str,
1969        bytes: Bytes,
1970        metadata: FileMetadata,
1971    ) -> Result<(), WorkspaceError> {
1972        let path = customer_path(path, self.checkout.volume_config())?;
1973        self.apply(vec![AuthoredMutation::CreateFile {
1974            path,
1975            bytes,
1976            metadata,
1977        }])
1978        .await
1979    }
1980
1981    /// Streams bytes into immutable content without changing the transaction's
1982    /// candidate namespace. Unused content remains a safe collectible orphan.
1983    ///
1984    /// # Errors
1985    ///
1986    /// Returns source, size, cancellation, allocation, or storage failures.
1987    pub async fn stage_content<R: crate::kernel::AsyncBlobSource>(
1988        &self,
1989        source: &mut R,
1990        maximum_source_bytes: u64,
1991    ) -> Result<crate::StagedContent, WorkspaceError> {
1992        self.stage_content_measured(
1993            source,
1994            maximum_source_bytes,
1995            crate::WorkBudget::UNBOUNDED,
1996            &crate::CancellationToken::new(),
1997        )
1998        .await
1999        .map(|receipt| receipt.value)
2000    }
2001
2002    pub(crate) async fn stage_content_measured<R: crate::kernel::AsyncBlobSource>(
2003        &self,
2004        source: &mut R,
2005        maximum_source_bytes: u64,
2006        budget: crate::WorkBudget,
2007        cancellation: &crate::CancellationToken,
2008    ) -> Result<crate::OperationReceipt<crate::StagedContent>, WorkspaceError> {
2009        self.checkout
2010            .stage_content(source, maximum_source_bytes, budget, cancellation)
2011            .await
2012            .map(|receipt| crate::OperationReceipt {
2013                value: receipt.value,
2014                work: receipt.work,
2015            })
2016            .map_err(WorkspaceError::engine)
2017    }
2018
2019    /// Creates every missing directory on one canonical absolute path.
2020    ///
2021    /// # Errors
2022    ///
2023    /// Rejects invalid paths, a non-directory prefix, or a bounded engine failure.
2024    pub async fn create_dir_all(&mut self, path: &str) -> Result<(), WorkspaceError> {
2025        let portable = PortablePath::parse(path, self.checkout.volume_config().limits)
2026            .map_err(WorkspaceError::path)?;
2027        let mut current = String::new();
2028        for component in portable.components() {
2029            current.push('/');
2030            current.push_str(component);
2031            let path = customer_path(&current, self.checkout.volume_config())?;
2032            let existing = self
2033                .checkout
2034                .lookup_no_follow(
2035                    &path,
2036                    crate::WorkBudget::UNBOUNDED,
2037                    &crate::CancellationToken::new(),
2038                )
2039                .await
2040                .map_err(WorkspaceError::engine)?
2041                .value
2042                .record;
2043            match existing {
2044                Some(value) if value.kind == FileKind::Directory => {}
2045                Some(_) => return Err(WorkspaceError::NotDirectory),
2046                None => {
2047                    self.apply(vec![AuthoredMutation::CreateDirectory {
2048                        path,
2049                        metadata: FileMetadata::default(),
2050                    }])
2051                    .await?;
2052                }
2053            }
2054        }
2055        Ok(())
2056    }
2057
2058    /// Creates exactly one empty directory with canonical metadata.
2059    ///
2060    /// # Errors
2061    ///
2062    /// Rejects invalid/existing paths or authenticated engine failures.
2063    pub async fn create_directory(&mut self, path: &str) -> Result<(), WorkspaceError> {
2064        let path = customer_path(path, self.checkout.volume_config())?;
2065        self.apply(vec![AuthoredMutation::CreateDirectory {
2066            path,
2067            metadata: FileMetadata::default(),
2068        }])
2069        .await
2070    }
2071
2072    /// Creates one symbolic link with opaque target bytes.
2073    ///
2074    /// # Errors
2075    ///
2076    /// Rejects invalid/existing paths, unsupported profiles, or engine failures.
2077    pub async fn create_symbolic_link(
2078        &mut self,
2079        path: &str,
2080        target: Bytes,
2081    ) -> Result<(), WorkspaceError> {
2082        let path = customer_path(path, self.checkout.volume_config())?;
2083        self.apply(vec![AuthoredMutation::CreateSymbolicLink {
2084            path,
2085            target,
2086            metadata: FileMetadata::default(),
2087        }])
2088        .await
2089    }
2090
2091    /// Creates one payload-free POSIX special node or exact device node.
2092    pub async fn create_special(
2093        &mut self,
2094        path: &str,
2095        kind: FileKind,
2096        device: Option<(u32, u32)>,
2097    ) -> Result<(), WorkspaceError> {
2098        let path = customer_path(path, self.checkout.volume_config())?;
2099        let mutation = match (kind, device) {
2100            (FileKind::Fifo | FileKind::Socket, None) => AuthoredMutation::CreateEmptySpecial {
2101                path,
2102                kind,
2103                metadata: FileMetadata::default(),
2104            },
2105            (FileKind::CharacterDevice | FileKind::BlockDevice, Some((major, minor))) => {
2106                AuthoredMutation::CreateDevice {
2107                    path,
2108                    kind,
2109                    major,
2110                    minor,
2111                    metadata: FileMetadata::default(),
2112                }
2113            }
2114            _ => return Err(WorkspaceError::IncompatibleWorkspace),
2115        };
2116        self.apply(vec![mutation]).await
2117    }
2118
2119    /// Creates or atomically replaces one complete UTF-8 file.
2120    ///
2121    /// # Errors
2122    ///
2123    /// Rejects invalid paths, non-regular destinations, size limits, and
2124    /// authenticated engine failures.
2125    pub async fn write_text(&mut self, path: &str, text: &str) -> Result<(), WorkspaceError> {
2126        self.write(path, Bytes::copy_from_slice(text.as_bytes()))
2127            .await
2128    }
2129
2130    /// Creates or atomically replaces one complete file.
2131    ///
2132    /// # Errors
2133    ///
2134    /// Rejects invalid paths, non-regular destinations, size limits, and
2135    /// authenticated engine failures.
2136    pub async fn write(&mut self, path: &str, bytes: Bytes) -> Result<(), WorkspaceError> {
2137        let path = customer_path(path, self.checkout.volume_config())?;
2138        let existing = self
2139            .checkout
2140            .lookup_no_follow(
2141                &path,
2142                crate::WorkBudget::UNBOUNDED,
2143                &crate::CancellationToken::new(),
2144            )
2145            .await
2146            .map_err(WorkspaceError::engine)?
2147            .value
2148            .record;
2149        let operations = match existing {
2150            None => vec![AuthoredMutation::CreateFile {
2151                path,
2152                bytes,
2153                metadata: FileMetadata::default(),
2154            }],
2155            Some(value) if value.kind == FileKind::Regular => vec![
2156                AuthoredMutation::Resize {
2157                    path: path.clone(),
2158                    logical_bytes: 0,
2159                },
2160                AuthoredMutation::Write {
2161                    path,
2162                    offset: 0,
2163                    bytes,
2164                },
2165            ],
2166            Some(_) => return Err(WorkspaceError::NotRegularFile),
2167        };
2168        self.apply(operations).await
2169    }
2170
2171    /// Creates or replaces one complete file by concatenating already staged
2172    /// authenticated content without copying part bodies.
2173    ///
2174    /// # Errors
2175    ///
2176    /// Rejects empty input, overflow, invalid destinations, and engine limits.
2177    pub async fn write_staged(
2178        &mut self,
2179        path: &str,
2180        parts: &[crate::StagedContent],
2181    ) -> Result<(), WorkspaceError> {
2182        if parts.is_empty() {
2183            return Err(WorkspaceError::EmptyContentSet);
2184        }
2185        let path = customer_path(path, self.checkout.volume_config())?;
2186        let existing = self
2187            .checkout
2188            .lookup_no_follow(
2189                &path,
2190                crate::WorkBudget::UNBOUNDED,
2191                &crate::CancellationToken::new(),
2192            )
2193            .await
2194            .map_err(WorkspaceError::engine)?
2195            .value
2196            .record;
2197        let mut operations = match existing {
2198            None => vec![AuthoredMutation::CreateFile {
2199                path: path.clone(),
2200                bytes: Bytes::new(),
2201                metadata: FileMetadata::default(),
2202            }],
2203            Some(record) if record.kind == FileKind::Regular => vec![AuthoredMutation::Resize {
2204                path: path.clone(),
2205                logical_bytes: 0,
2206            }],
2207            Some(_) => return Err(WorkspaceError::NotRegularFile),
2208        };
2209        let mut offset = 0_u64;
2210        for part in parts {
2211            if part.logical_bytes() != 0 {
2212                operations.push(AuthoredMutation::WriteFromContent {
2213                    path: path.clone(),
2214                    offset,
2215                    content: *part,
2216                });
2217            }
2218            offset = offset
2219                .checked_add(part.logical_bytes())
2220                .ok_or(WorkspaceError::ContentLengthOverflow)?;
2221        }
2222        self.apply(operations).await
2223    }
2224
2225    /// Removes one existing namespace binding from this transaction.
2226    ///
2227    /// # Errors
2228    ///
2229    /// Rejects invalid or absent paths and authenticated engine failures.
2230    pub async fn remove(&mut self, path: &str) -> Result<(), WorkspaceError> {
2231        let path = customer_path(path, self.checkout.volume_config())?;
2232        let existing = self
2233            .checkout
2234            .lookup_no_follow(
2235                &path,
2236                crate::WorkBudget::UNBOUNDED,
2237                &crate::CancellationToken::new(),
2238            )
2239            .await
2240            .map_err(WorkspaceError::engine)?
2241            .value
2242            .record
2243            .ok_or(WorkspaceError::NotFound)?;
2244        self.apply(vec![AuthoredMutation::Remove {
2245            path,
2246            expected_file_id: Some(existing.file_id),
2247        }])
2248        .await
2249    }
2250
2251    /// Removes one binding only when its authenticated identity still matches.
2252    ///
2253    /// The precondition is compiled into the same atomic checkout mutation as
2254    /// the removal; callers never perform a check-then-delete sequence.
2255    pub async fn remove_if(
2256        &mut self,
2257        path: &str,
2258        expected_file_id: crate::FileId,
2259    ) -> Result<(), WorkspaceError> {
2260        let path = customer_path(path, self.checkout.volume_config())?;
2261        self.apply(vec![AuthoredMutation::Remove {
2262            path,
2263            expected_file_id: Some(expected_file_id),
2264        }])
2265        .await
2266    }
2267
2268    /// Replaces one destination with a complete copy-on-write clone.
2269    ///
2270    /// # Errors
2271    ///
2272    /// Rejects invalid paths, non-regular endpoints, and bounded engine errors.
2273    pub async fn copy(&mut self, source: &str, destination: &str) -> Result<(), WorkspaceError> {
2274        let config = self.checkout.volume_config();
2275        let source = customer_path(source, config)?;
2276        let destination = customer_path(destination, config)?;
2277        let source_record = self
2278            .checkout
2279            .lookup_no_follow(
2280                &source,
2281                crate::WorkBudget::UNBOUNDED,
2282                &crate::CancellationToken::new(),
2283            )
2284            .await
2285            .map_err(WorkspaceError::engine)?
2286            .value
2287            .record
2288            .ok_or(WorkspaceError::NotFound)?;
2289        let logical_bytes = regular_file_bytes(source_record)?;
2290        let destination_record = self
2291            .checkout
2292            .lookup_no_follow(
2293                &destination,
2294                crate::WorkBudget::UNBOUNDED,
2295                &crate::CancellationToken::new(),
2296            )
2297            .await
2298            .map_err(WorkspaceError::engine)?
2299            .value
2300            .record;
2301        let mut operations = match destination_record {
2302            None => vec![AuthoredMutation::CreateFile {
2303                path: destination.clone(),
2304                bytes: Bytes::new(),
2305                metadata: FileMetadata::default(),
2306            }],
2307            Some(record) if record.kind == FileKind::Regular => vec![AuthoredMutation::Resize {
2308                path: destination.clone(),
2309                logical_bytes: 0,
2310            }],
2311            Some(_) => return Err(WorkspaceError::NotRegularFile),
2312        };
2313        if logical_bytes != 0 {
2314            operations.push(AuthoredMutation::CloneRange(crate::FileCloneRequest {
2315                source,
2316                source_offset: 0,
2317                destination,
2318                destination_offset: 0,
2319                length: logical_bytes,
2320            }));
2321        }
2322        self.apply(operations).await
2323    }
2324
2325    /// Atomically renames one namespace binding within the workspace.
2326    ///
2327    /// # Errors
2328    ///
2329    /// Rejects invalid or missing paths and bounded engine failures.
2330    pub async fn rename(&mut self, source: &str, destination: &str) -> Result<(), WorkspaceError> {
2331        self.rename_with_replace(source, destination, true).await
2332    }
2333
2334    /// Atomically renames one namespace binding with an exact replacement policy.
2335    ///
2336    /// # Errors
2337    ///
2338    /// Rejects invalid or missing paths, a disallowed existing destination,
2339    /// and bounded engine failures.
2340    pub async fn rename_with_replace(
2341        &mut self,
2342        source: &str,
2343        destination: &str,
2344        replace: bool,
2345    ) -> Result<(), WorkspaceError> {
2346        let config = self.checkout.volume_config();
2347        self.apply(vec![AuthoredMutation::Rename {
2348            source: customer_path(source, config)?,
2349            destination: customer_path(destination, config)?,
2350            replace,
2351        }])
2352        .await
2353    }
2354
2355    /// Creates one same-workspace hard link without copying file content.
2356    ///
2357    /// # Errors
2358    ///
2359    /// Rejects invalid/foreign paths, incompatible kinds, or engine failures.
2360    pub async fn hard_link(
2361        &mut self,
2362        source: &str,
2363        destination: &str,
2364    ) -> Result<(), WorkspaceError> {
2365        let config = self.checkout.volume_config();
2366        self.apply(vec![AuthoredMutation::HardLink {
2367            source: customer_path(source, config)?,
2368            destination: customer_path(destination, config)?,
2369        }])
2370        .await
2371    }
2372
2373    /// Replaces one regular-file range without rewriting untouched extents.
2374    ///
2375    /// # Errors
2376    ///
2377    /// Rejects invalid paths, overflow, incompatible kinds, or engine failures.
2378    pub async fn write_range(
2379        &mut self,
2380        path: &str,
2381        offset: u64,
2382        bytes: Bytes,
2383    ) -> Result<(), WorkspaceError> {
2384        let path = customer_path(path, self.checkout.volume_config())?;
2385        self.apply(vec![AuthoredMutation::Write {
2386            path,
2387            offset,
2388            bytes,
2389        }])
2390        .await
2391    }
2392
2393    /// Changes one regular file's logical length while preserving sparse layout.
2394    ///
2395    /// # Errors
2396    ///
2397    /// Rejects invalid paths, incompatible kinds, limits, or engine failures.
2398    pub async fn resize(&mut self, path: &str, logical_bytes: u64) -> Result<(), WorkspaceError> {
2399        let path = customer_path(path, self.checkout.volume_config())?;
2400        self.apply(vec![AuthoredMutation::Resize {
2401            path,
2402            logical_bytes,
2403        }])
2404        .await
2405    }
2406
2407    /// Punches a hole or installs allocated zeros over one exact range.
2408    ///
2409    /// # Errors
2410    ///
2411    /// Rejects invalid ranges/paths, incompatible kinds, or engine failures.
2412    pub async fn zero_range(
2413        &mut self,
2414        path: &str,
2415        range: crate::ByteRange,
2416        allocated: bool,
2417        extend: bool,
2418    ) -> Result<(), WorkspaceError> {
2419        let path = customer_path(path, self.checkout.volume_config())?;
2420        self.apply(vec![AuthoredMutation::ZeroRange {
2421            path,
2422            range,
2423            allocated,
2424            extend,
2425        }])
2426        .await
2427    }
2428
2429    /// Preallocates one sparse range without replacing existing content.
2430    ///
2431    /// # Errors
2432    ///
2433    /// Rejects invalid ranges/paths, incompatible kinds, or engine failures.
2434    pub async fn preallocate(
2435        &mut self,
2436        path: &str,
2437        range: crate::ByteRange,
2438        keep_size: bool,
2439    ) -> Result<(), WorkspaceError> {
2440        let path = customer_path(path, self.checkout.volume_config())?;
2441        self.apply(vec![AuthoredMutation::Preallocate {
2442            path,
2443            range,
2444            keep_size,
2445        }])
2446        .await
2447    }
2448
2449    /// Clones one immutable range without reading or copying content bytes.
2450    ///
2451    /// # Errors
2452    ///
2453    /// Rejects invalid ranges/paths, incompatible files, or engine failures.
2454    pub async fn clone_range(
2455        &mut self,
2456        source: &str,
2457        source_offset: u64,
2458        destination: &str,
2459        destination_offset: u64,
2460        length: u64,
2461    ) -> Result<(), WorkspaceError> {
2462        let config = self.checkout.volume_config();
2463        self.apply(vec![AuthoredMutation::CloneRange(
2464            crate::FileCloneRequest {
2465                source: customer_path(source, config)?,
2466                source_offset,
2467                destination: customer_path(destination, config)?,
2468                destination_offset,
2469                length,
2470            },
2471        )])
2472        .await
2473    }
2474
2475    /// Replaces complete canonical metadata for one existing path.
2476    ///
2477    /// # Errors
2478    ///
2479    /// Rejects invalid/absent paths, unsupported metadata, or engine failures.
2480    pub async fn set_metadata(
2481        &mut self,
2482        path: &str,
2483        metadata: FileMetadata,
2484    ) -> Result<(), WorkspaceError> {
2485        let path = customer_path(path, self.checkout.volume_config())?;
2486        self.apply(vec![AuthoredMutation::SetMetadata { path, metadata }])
2487            .await
2488    }
2489
2490    /// Publishes the complete candidate with one stable idempotency key.
2491    /// Conflicts retain the candidate in `self` for explicit rebase or retry.
2492    ///
2493    /// # Errors
2494    ///
2495    /// Returns authentication, closure, storage, cancellation, or indeterminate
2496    /// authority failures. Semantic publication rejections are typed outcomes.
2497    pub async fn commit(&mut self) -> Result<TransactionCommit<A, O>, WorkspaceError> {
2498        self.commit_with_permit(crate::PublicationPermit::Unrestricted)
2499            .await
2500    }
2501
2502    /// Publishes this transaction only while an SDK operation-window permit
2503    /// is still active at the authority linearization point.
2504    pub async fn commit_with_permit(
2505        &mut self,
2506        permit: crate::PublicationPermit,
2507    ) -> Result<TransactionCommit<A, O>, WorkspaceError> {
2508        self.commit_with_permit_measured(
2509            permit,
2510            crate::WorkBudget::UNBOUNDED,
2511            &crate::CancellationToken::new(),
2512        )
2513        .await
2514        .map(|receipt| receipt.value)
2515    }
2516
2517    /// Publishes this transaction under an exact work budget and cancellation token.
2518    pub(crate) async fn commit_with_permit_measured(
2519        &mut self,
2520        permit: crate::PublicationPermit,
2521        budget: crate::WorkBudget,
2522        cancellation: &crate::CancellationToken,
2523    ) -> Result<crate::OperationReceipt<TransactionCommit<A, O>>, WorkspaceError> {
2524        cancellation.check().map_err(WorkspaceError::from)?;
2525        if self.requires_rebase {
2526            let receipt = Box::pin(self.checkout.retry_stale_commit(
2527                self.idempotency_key.operation_id(),
2528                permit,
2529                budget,
2530                cancellation,
2531            ))
2532            .await
2533            .map_err(|failure| WorkspaceError::from(failure.error))?;
2534            let work = receipt.work;
2535            let Some(outcome) = receipt.value else {
2536                let actual = Box::pin(self.workspace.head_measured(
2537                    work.remaining(budget).map_err(WorkspaceError::from)?,
2538                    cancellation,
2539                ))
2540                .await?;
2541                return Ok(crate::OperationReceipt {
2542                    value: TransactionCommit::Conflict {
2543                        actual: actual.value,
2544                    },
2545                    work: work
2546                        .checked_add(actual.work)
2547                        .map_err(WorkspaceError::from)?,
2548                });
2549            };
2550            return Box::pin(self.commit_outcome_measured(outcome, work, budget, cancellation))
2551                .await;
2552        }
2553        let receipt = Box::pin(self.checkout.commit_with_permit(
2554            self.idempotency_key.operation_id(),
2555            permit,
2556            budget,
2557            cancellation,
2558        ))
2559        .await
2560        .map_err(|failure| WorkspaceError::from(failure.error))?;
2561        Box::pin(self.commit_outcome_measured(receipt.value, receipt.work, budget, cancellation))
2562            .await
2563    }
2564
2565    async fn commit_outcome_measured(
2566        &self,
2567        outcome: CheckoutCommitOutcome,
2568        work: crate::WorkCounters,
2569        budget: crate::WorkBudget,
2570        cancellation: &crate::CancellationToken,
2571    ) -> Result<crate::OperationReceipt<TransactionCommit<A, O>>, WorkspaceError> {
2572        let value = match outcome {
2573            CheckoutCommitOutcome::Committed { generation_id, .. } => {
2574                TransactionCommit::Committed(Generation {
2575                    workspace: self.workspace.clone(),
2576                    id: generation_id,
2577                })
2578            }
2579            CheckoutCommitOutcome::AlreadyCommitted { generation_id, .. } => {
2580                TransactionCommit::AlreadyCommitted(Generation {
2581                    workspace: self.workspace.clone(),
2582                    id: generation_id,
2583                })
2584            }
2585            CheckoutCommitOutcome::Conflict { .. } => {
2586                let actual = Box::pin(self.workspace.head_measured(
2587                    work.remaining(budget).map_err(WorkspaceError::from)?,
2588                    cancellation,
2589                ))
2590                .await?;
2591                return Ok(crate::OperationReceipt {
2592                    value: TransactionCommit::Conflict {
2593                        actual: actual.value,
2594                    },
2595                    work: work
2596                        .checked_add(actual.work)
2597                        .map_err(WorkspaceError::from)?,
2598                });
2599            }
2600            CheckoutCommitOutcome::Fenced { .. } => TransactionCommit::Fenced,
2601            CheckoutCommitOutcome::IdempotencyConflict { .. } => {
2602                TransactionCommit::IdempotencyConflict
2603            }
2604        };
2605        Ok(crate::OperationReceipt { value, work })
2606    }
2607
2608    /// Safely advances this retained sparse candidate to the current workspace
2609    /// head and replays only its local mutations.
2610    ///
2611    /// # Errors
2612    ///
2613    /// Returns bounded dependency-probe, authentication, storage, or replay
2614    /// failures. Semantic overlap is returned as a typed conflict and leaves
2615    /// the transaction unchanged.
2616    pub async fn rebase(
2617        &mut self,
2618        maximum_conflicts: u32,
2619    ) -> Result<TransactionRebase<A, O>, WorkspaceError> {
2620        let decision = self
2621            .checkout
2622            .rebase_head(
2623                maximum_conflicts,
2624                crate::WorkBudget::UNBOUNDED,
2625                &crate::CancellationToken::new(),
2626            )
2627            .await
2628            .map_err(WorkspaceError::engine)?
2629            .value;
2630        Ok(match decision {
2631            crate::kernel::RebaseDecision::Safe { generation } => {
2632                self.requires_rebase = false;
2633                TransactionRebase::Rebased(Generation {
2634                    workspace: self.workspace.clone(),
2635                    id: generation,
2636                })
2637            }
2638            crate::kernel::RebaseDecision::Conflicted {
2639                conflicts,
2640                truncated,
2641            } => TransactionRebase::Conflicted {
2642                conflicts: conflicts
2643                    .into_iter()
2644                    .map(TransactionConflict::from_engine)
2645                    .collect(),
2646                truncated,
2647            },
2648        })
2649    }
2650
2651    async fn apply(&mut self, operations: Vec<AuthoredMutation>) -> Result<(), WorkspaceError> {
2652        self.apply_measured(
2653            operations,
2654            crate::WorkBudget::UNBOUNDED,
2655            &crate::CancellationToken::new(),
2656        )
2657        .await
2658        .map(|_| ())
2659    }
2660
2661    async fn apply_measured(
2662        &mut self,
2663        operations: Vec<AuthoredMutation>,
2664        budget: crate::WorkBudget,
2665        cancellation: &crate::CancellationToken,
2666    ) -> Result<crate::WorkCounters, WorkspaceError> {
2667        self.checkout
2668            .apply_authored_transaction(operations, budget, cancellation)
2669            .await
2670            .map(|receipt| receipt.work)
2671            .map_err(|failure| WorkspaceError::from(failure.error))
2672    }
2673}
2674
2675/// Terminal result of one atomic workspace transaction publication.
2676pub enum TransactionCommit<A, O> {
2677    /// This call published the generation.
2678    Committed(Generation<A, O>),
2679    /// The same key and transaction were already durable.
2680    AlreadyCommitted(Generation<A, O>),
2681    /// Another writer changed an observed dependency first.
2682    Conflict {
2683        /// Actual immutable workspace head.
2684        actual: Generation<A, O>,
2685    },
2686    /// This writer was superseded by a newer authority epoch.
2687    Fenced,
2688    /// The key was previously bound to different transaction input.
2689    IdempotencyConflict,
2690}
2691
2692/// Observation-safe advancement of one retained transaction candidate.
2693pub enum TransactionRebase<A, O> {
2694    /// The current head is now the candidate's immutable base and its sparse
2695    /// local mutations were replayed without crossing a dependency.
2696    Rebased(Generation<A, O>),
2697    /// One or more exact observed or mutated regions changed upstream.
2698    Conflicted {
2699        /// Stable region-specific conflicts bounded by the caller.
2700        conflicts: Vec<TransactionConflict>,
2701        /// Additional conflicts exceeded the retained result bound.
2702        truncated: bool,
2703    },
2704}
2705
2706/// Terminal outcome of advancing one fork onto its source workspace.
2707pub enum WorkspaceRebase<A, O> {
2708    /// A new rebased generation became durable.
2709    Rebased(Generation<A, O>),
2710    /// The identical retry was already durable.
2711    AlreadyRebased(Generation<A, O>),
2712    /// The fork already includes the source's current generation.
2713    Current(Generation<A, O>),
2714    /// This workspace changed after rebase planning; retry from the new head.
2715    Stale(Generation<A, O>),
2716    /// Local and upstream semantic changes overlap.
2717    Conflicted {
2718        /// Stable path-independent conflict regions.
2719        conflicts: Vec<MergeConflict>,
2720        /// Additional conflicts exceeded the retained bound.
2721        truncated: bool,
2722    },
2723    /// Writer ownership changed before publication.
2724    Fenced,
2725    /// The retry identity was previously bound to another input.
2726    IdempotencyConflict,
2727}
2728
2729/// One exact customer-visible dependency conflict.
2730#[derive(Clone, Debug, Eq, PartialEq)]
2731pub struct TransactionConflict {
2732    /// Exact semantic region that changed.
2733    pub region: TransactionConflictRegion,
2734    /// Whether local work observed, mutated, or both observed and mutated it.
2735    pub usage: TransactionDependencyUse,
2736    /// Digest of the old bounded semantic state; `None` means absent.
2737    pub expected: Option<crate::Digest>,
2738    /// Digest of the current bounded semantic state; `None` means absent.
2739    pub actual: Option<crate::Digest>,
2740}
2741
2742/// Exact region retained by an observation-safe transaction.
2743#[derive(Clone, Debug, Eq, PartialEq)]
2744pub enum TransactionConflictRegion {
2745    /// Complete path-independent file record.
2746    FileRecord(crate::FileId),
2747    /// Complete file metadata.
2748    Metadata(crate::FileId),
2749    /// Exact logical file length.
2750    FileLength(crate::FileId),
2751    /// Exact non-empty byte range.
2752    ContentRange {
2753        /// Stable file identity.
2754        file_id: crate::FileId,
2755        /// Inclusive logical offset.
2756        offset: u64,
2757        /// Positive byte length.
2758        length: u64,
2759    },
2760    /// One sparse seek observation.
2761    SparseSeek {
2762        /// Stable file identity.
2763        file_id: crate::FileId,
2764        /// Inclusive logical starting offset.
2765        offset: u64,
2766        /// Sparse class that was observed.
2767        target: TransactionSparseSeek,
2768    },
2769    /// One exact directory binding, including absence.
2770    DirectoryName {
2771        /// Stable parent directory identity.
2772        directory_id: crate::FileId,
2773        /// Exact canonical child name.
2774        name: LogicalName,
2775    },
2776    /// One exact bounded directory cursor interval.
2777    DirectoryRange {
2778        /// Stable parent directory identity.
2779        directory_id: crate::FileId,
2780        /// Exclusive lower cursor.
2781        after: Option<LogicalName>,
2782        /// Exact page bound that was observed.
2783        maximum_entries: u32,
2784    },
2785}
2786
2787/// How one local transaction depended on a conflicting region.
2788#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2789pub enum TransactionDependencyUse {
2790    /// Read-only observation.
2791    Observation,
2792    /// Mutation precondition.
2793    Mutation,
2794    /// Both observation and mutation precondition.
2795    ObservationAndMutation,
2796}
2797
2798/// Sparse query class retained by a transaction dependency.
2799#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2800pub enum TransactionSparseSeek {
2801    /// Seek to represented data.
2802    Data,
2803    /// Seek to an unallocated hole.
2804    Hole,
2805}
2806
2807impl TransactionConflict {
2808    fn from_engine(value: crate::kernel::RebaseConflict) -> Self {
2809        use crate::kernel::{DependencyRegion, DependencyState, DependencyUse, ExtentSeekTarget};
2810        let state = |value| match value {
2811            DependencyState::Absent => None,
2812            DependencyState::Present(digest) => Some(digest),
2813        };
2814        Self {
2815            region: match value.region {
2816                DependencyRegion::FileRecord(id) => TransactionConflictRegion::FileRecord(id),
2817                DependencyRegion::Metadata(id) => TransactionConflictRegion::Metadata(id),
2818                DependencyRegion::FileLength(id) => TransactionConflictRegion::FileLength(id),
2819                DependencyRegion::ContentRange {
2820                    file_id,
2821                    offset,
2822                    length,
2823                } => TransactionConflictRegion::ContentRange {
2824                    file_id,
2825                    offset,
2826                    length,
2827                },
2828                DependencyRegion::SparseSeek {
2829                    file_id,
2830                    offset,
2831                    target,
2832                } => TransactionConflictRegion::SparseSeek {
2833                    file_id,
2834                    offset,
2835                    target: match target {
2836                        ExtentSeekTarget::Data => TransactionSparseSeek::Data,
2837                        ExtentSeekTarget::Hole => TransactionSparseSeek::Hole,
2838                    },
2839                },
2840                DependencyRegion::DirectoryName { directory_id, name } => {
2841                    TransactionConflictRegion::DirectoryName { directory_id, name }
2842                }
2843                DependencyRegion::DirectoryRange {
2844                    directory_id,
2845                    after,
2846                    maximum_entries,
2847                } => TransactionConflictRegion::DirectoryRange {
2848                    directory_id,
2849                    after,
2850                    maximum_entries,
2851                },
2852            },
2853            usage: match value.usage {
2854                DependencyUse::Observation => TransactionDependencyUse::Observation,
2855                DependencyUse::Mutation => TransactionDependencyUse::Mutation,
2856                DependencyUse::ObservationAndMutation => {
2857                    TransactionDependencyUse::ObservationAndMutation
2858                }
2859            },
2860            expected: state(value.expected),
2861            actual: state(value.actual),
2862        }
2863    }
2864}
2865
2866/// Immutable complete filesystem state belonging to one workspace.
2867pub struct Generation<A, O> {
2868    pub(crate) workspace: Workspace<A, O>,
2869    pub(crate) id: GenerationId,
2870}
2871
2872/// Exact synchronization result for one workspace head observation.
2873pub struct WorkspaceSync<A, O> {
2874    generation: Generation<A, O>,
2875}
2876
2877impl<A, O> WorkspaceSync<A, O> {
2878    /// Immutable generation selected at the synchronization boundary.
2879    #[must_use]
2880    pub const fn generation(&self) -> &Generation<A, O> {
2881        &self.generation
2882    }
2883
2884    /// Consumes the receipt and returns its immutable generation.
2885    #[must_use]
2886    pub fn into_generation(self) -> Generation<A, O> {
2887        self.generation
2888    }
2889}
2890
2891/// Durable human-readable retention of one exact immutable generation.
2892pub struct Checkpoint<A, O> {
2893    label: WorkspaceName,
2894    generation: Generation<A, O>,
2895}
2896
2897/// Durable opaque retention of one exact immutable generation.
2898pub struct GenerationPin<A, O> {
2899    identity: WorkspaceName,
2900    generation: Generation<A, O>,
2901}
2902
2903/// Terminal result of deleting one named workspace head.
2904#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2905pub enum WorkspaceDelete {
2906    /// The terminal deletion fact became durable.
2907    Deleted,
2908    /// The workspace was already terminally deleted.
2909    AlreadyDeleted,
2910    /// Another authority operation won; reopen before retrying.
2911    Conflict,
2912    /// The retry identity was previously bound to different input.
2913    IdempotencyConflict,
2914}
2915
2916/// Complete customer-visible facts for one path without following links.
2917#[derive(Clone, Debug, Eq, PartialEq)]
2918pub struct WorkspaceStat {
2919    /// Stable path-independent identity.
2920    pub file_id: crate::FileId,
2921    /// Exact filesystem object kind.
2922    pub kind: FileKind,
2923    /// Number of namespace bindings to this identity.
2924    pub link_count: u64,
2925    /// Logical bytes for regular files and symbolic links.
2926    pub logical_bytes: Option<u64>,
2927    /// Complete scalar cross-profile metadata and explicit opaque-payload presence.
2928    pub metadata: WorkspaceMetadata,
2929}
2930
2931/// Customer-visible metadata without storage object identities.
2932///
2933/// `None` means the source profile did not represent or observe the fact; zero
2934/// remains an exact represented value. Opaque ACL, security-descriptor, and
2935/// named-attribute contents are accessed through their dedicated bounded APIs.
2936#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
2937pub struct WorkspaceMetadata {
2938    /// POSIX permission/type bits.
2939    pub posix_mode: Option<u32>,
2940    /// POSIX numeric owner identity.
2941    pub posix_uid: Option<u32>,
2942    /// POSIX numeric group identity.
2943    pub posix_gid: Option<u32>,
2944    /// POSIX inode flags.
2945    pub posix_flags: Option<u64>,
2946    /// Windows file-attribute bitset.
2947    pub windows_attributes: Option<u32>,
2948    /// Creation/birth time in signed Unix-epoch nanoseconds.
2949    pub created_ns: Option<i64>,
2950    /// Last content-modification time in signed Unix-epoch nanoseconds.
2951    pub modified_ns: Option<i64>,
2952    /// Last access time in signed Unix-epoch nanoseconds.
2953    pub accessed_ns: Option<i64>,
2954    /// Last metadata-change time in signed Unix-epoch nanoseconds.
2955    pub changed_ns: Option<i64>,
2956    /// Whether authenticated named attributes exist.
2957    pub has_named_attributes: bool,
2958    /// Whether an authenticated ACL exists.
2959    pub has_acl: bool,
2960    /// Whether an authenticated Windows security descriptor exists.
2961    pub has_security_descriptor: bool,
2962}
2963
2964impl WorkspaceMetadata {
2965    pub(crate) fn from_engine(metadata: FileMetadata) -> Self {
2966        Self {
2967            posix_mode: metadata_value(metadata.posix_mode),
2968            posix_uid: metadata_value(metadata.posix_uid),
2969            posix_gid: metadata_value(metadata.posix_gid),
2970            posix_flags: metadata_value(metadata.posix_flags),
2971            windows_attributes: metadata_value(metadata.windows_attributes),
2972            created_ns: metadata_value(metadata.created_ns),
2973            modified_ns: metadata_value(metadata.modified_ns),
2974            accessed_ns: metadata_value(metadata.accessed_ns),
2975            changed_ns: metadata_value(metadata.changed_ns),
2976            has_named_attributes: matches!(metadata.named_attributes, MetadataField::Value(_)),
2977            has_acl: matches!(metadata.acl, MetadataField::Value(_)),
2978            has_security_descriptor: matches!(
2979                metadata.security_descriptor,
2980                MetadataField::Value(_)
2981            ),
2982        }
2983    }
2984}
2985
2986const fn metadata_value<T: Copy>(field: MetadataField<T>) -> Option<T> {
2987    match field {
2988        MetadataField::Unavailable => None,
2989        MetadataField::Value(value) => Some(value),
2990    }
2991}
2992
2993/// One exact child binding in an authenticated directory page.
2994#[derive(Clone, Debug, Eq, PartialEq)]
2995pub struct WorkspaceDirectoryEntry {
2996    /// Exact profile-aware name.
2997    pub name: LogicalName,
2998    /// Stable path-independent target identity.
2999    pub file_id: crate::FileId,
3000    /// Exact target kind.
3001    pub kind: FileKind,
3002}
3003
3004/// One bounded authenticated directory page.
3005#[derive(Clone, Debug, Eq, PartialEq)]
3006pub struct WorkspaceDirectoryPage {
3007    /// Entries strictly after the supplied cursor.
3008    pub entries: Vec<WorkspaceDirectoryEntry>,
3009    /// Whether another page exists.
3010    pub has_more: bool,
3011}
3012
3013/// Customer-visible sparse representation class.
3014#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3015pub enum WorkspaceExtentKind {
3016    /// Unallocated logical bytes.
3017    Hole,
3018    /// Physically represented zeros.
3019    AllocatedZero,
3020    /// Immutable content bytes.
3021    Content,
3022}
3023
3024/// One clipped sparse logical span.
3025#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3026pub struct WorkspaceExtentSpan {
3027    /// Inclusive logical offset.
3028    pub offset: u64,
3029    /// Positive span length.
3030    pub length: u64,
3031    /// Exclusive end of the complete source extent.
3032    pub source_end: u64,
3033    /// Physical representation class without backend identity.
3034    pub kind: WorkspaceExtentKind,
3035}
3036
3037/// Bounded sparse plan without storage topology or object identities.
3038#[derive(Clone, Debug, Eq, PartialEq)]
3039pub struct WorkspaceExtentPlan {
3040    /// Ordered contiguous spans covering the request.
3041    pub spans: Vec<WorkspaceExtentSpan>,
3042}
3043
3044/// Immutable semantic delta between two exact generations.
3045pub struct ChangeSet<A, O> {
3046    from: Generation<A, O>,
3047    to: Generation<A, O>,
3048    changes: GenerationDiff,
3049    work: crate::WorkCounters,
3050}
3051
3052/// One changed exact path between two immutable generations.
3053#[derive(Clone, Debug, Eq, PartialEq)]
3054pub struct ChangedPath {
3055    /// Canonical exact path in either endpoint.
3056    pub path: NamespacePath,
3057    /// Complete path-independent record in the earlier generation, when present.
3058    pub before: Option<crate::kernel::FileRecord>,
3059    /// Complete path-independent record in the later generation, when present.
3060    pub after: Option<crate::kernel::FileRecord>,
3061}
3062
3063impl<A, O> ChangeSet<A, O> {
3064    /// Exact immutable base endpoint.
3065    #[must_use]
3066    pub const fn from(&self) -> &Generation<A, O> {
3067        &self.from
3068    }
3069
3070    /// Exact immutable resulting endpoint.
3071    #[must_use]
3072    pub const fn to(&self) -> &Generation<A, O> {
3073        &self.to
3074    }
3075
3076    /// Stable path-independent records and namespace binding changes.
3077    #[must_use]
3078    pub const fn changes(&self) -> &GenerationDiff {
3079        &self.changes
3080    }
3081
3082    /// Exact bounded work used to authenticate and compute this delta.
3083    #[must_use]
3084    pub const fn work(&self) -> crate::WorkCounters {
3085        self.work
3086    }
3087}
3088
3089fn merge_binding_endpoint(
3090    paths: &mut BTreeMap<
3091        NamespacePath,
3092        (
3093            Option<crate::kernel::FileRecord>,
3094            Option<crate::kernel::FileRecord>,
3095        ),
3096    >,
3097    directories: &BTreeMap<FileId, Vec<(NamespacePath, crate::kernel::FileRecord)>>,
3098    records: &BTreeMap<FileId, crate::kernel::FileRecord>,
3099    change: &crate::DirectoryBindingChange,
3100    limits: crate::model::VolumeLimits,
3101    before: bool,
3102) -> Result<(), WorkspaceError> {
3103    let Some(directories) = directories.get(&change.directory_id) else {
3104        return Ok(());
3105    };
3106    let binding = if before {
3107        change.before.as_ref()
3108    } else {
3109        change.after.as_ref()
3110    };
3111    let record = binding.and_then(|entry| records.get(&entry.file_id).copied());
3112    for (directory, _) in directories {
3113        let path = child_namespace_path(directory, change.name.clone(), limits)?;
3114        let endpoints = paths.entry(path).or_default();
3115        if before {
3116            endpoints.0 = record;
3117        } else {
3118            endpoints.1 = record;
3119        }
3120    }
3121    Ok(())
3122}
3123
3124struct NamespaceRecordSearch {
3125    records: BTreeMap<FileId, Vec<(NamespacePath, crate::kernel::FileRecord)>>,
3126    complete: bool,
3127}
3128
3129impl<A: AsyncAuthorityStore, O: AsyncObjectStore> ChangeSet<A, O> {
3130    /// Resolves every changed record or binding to exact paths with one bounded traversal of each
3131    /// endpoint. Equal Merkle subtrees remain skipped by the semantic diff that created this set.
3132    ///
3133    /// # Errors
3134    ///
3135    /// Returns [`WorkspaceError::ChangedPathLimit`] instead of a partial result when either
3136    /// endpoint exceeds `maximum_entries`.
3137    pub async fn changed_paths(
3138        &self,
3139        maximum_entries: u32,
3140    ) -> Result<Vec<ChangedPath>, WorkspaceError> {
3141        self.changed_paths_bounded(
3142            maximum_entries,
3143            crate::WorkBudget::UNBOUNDED,
3144            &crate::CancellationToken::new(),
3145        )
3146        .await
3147        .map(|receipt| receipt.value)
3148    }
3149
3150    /// Resolves changed paths with one cumulative work budget and explicit
3151    /// cancellation token. Partial traversal is never reported as exact.
3152    #[allow(clippy::too_many_lines)]
3153    pub async fn changed_paths_bounded(
3154        &self,
3155        maximum_entries: u32,
3156        budget: crate::WorkBudget,
3157        cancellation: &crate::CancellationToken,
3158    ) -> Result<crate::OperationReceipt<Vec<ChangedPath>>, WorkspaceError> {
3159        if self.changes.truncated {
3160            return Err(WorkspaceError::ChangedPathLimit);
3161        }
3162        let file_ids: BTreeSet<_> = self
3163            .changes
3164            .files
3165            .iter()
3166            .map(|change| change.file_id)
3167            .chain(
3168                self.changes
3169                    .bindings
3170                    .iter()
3171                    .map(|change| change.directory_id),
3172            )
3173            .chain(self.changes.bindings.iter().flat_map(|change| {
3174                change
3175                    .before
3176                    .iter()
3177                    .chain(change.after.iter())
3178                    .map(|entry| entry.file_id)
3179            }))
3180            .collect();
3181        self.work.verify(budget).map_err(WorkspaceError::engine)?;
3182        let before_receipt = self
3183            .from
3184            .namespace_records_for_file_ids_bounded(
3185                file_ids.iter().copied(),
3186                maximum_entries,
3187                self.work
3188                    .remaining(budget)
3189                    .map_err(WorkspaceError::engine)?,
3190                cancellation,
3191            )
3192            .await?;
3193        let mut work = self
3194            .work
3195            .checked_add(before_receipt.work)
3196            .map_err(WorkspaceError::engine)?;
3197        let after_receipt = self
3198            .to
3199            .namespace_records_for_file_ids_bounded(
3200                file_ids.iter().copied(),
3201                maximum_entries,
3202                work.remaining(budget).map_err(WorkspaceError::engine)?,
3203                cancellation,
3204            )
3205            .await?;
3206        work = work
3207            .checked_add(after_receipt.work)
3208            .map_err(WorkspaceError::engine)?;
3209        let before = before_receipt.value;
3210        let after = after_receipt.value;
3211        if !before.complete || !after.complete {
3212            return Err(WorkspaceError::ChangedPathLimit);
3213        }
3214        let before = before.records;
3215        let after = after.records;
3216        let mut paths = BTreeMap::<NamespacePath, (Option<_>, Option<_>)>::new();
3217        let before_records: BTreeMap<_, _> = before
3218            .iter()
3219            .filter_map(|(&file_id, records)| records.first().map(|(_, record)| (file_id, *record)))
3220            .collect();
3221        let after_records: BTreeMap<_, _> = after
3222            .iter()
3223            .filter_map(|(&file_id, records)| records.first().map(|(_, record)| (file_id, *record)))
3224            .collect();
3225        for records in before.values() {
3226            for (path, record) in records {
3227                paths.entry(path.clone()).or_default().0 = Some(*record);
3228            }
3229        }
3230        for records in after.values() {
3231            for (path, record) in records {
3232                paths.entry(path.clone()).or_default().1 = Some(*record);
3233            }
3234        }
3235        for change in &self.changes.bindings {
3236            merge_binding_endpoint(
3237                &mut paths,
3238                &before,
3239                &before_records,
3240                change,
3241                self.from.workspace.volume.config.limits,
3242                true,
3243            )?;
3244            merge_binding_endpoint(
3245                &mut paths,
3246                &after,
3247                &after_records,
3248                change,
3249                self.to.workspace.volume.config.limits,
3250                false,
3251            )?;
3252        }
3253        let paths = paths
3254            .into_iter()
3255            .filter(|(_, (before, after))| match (before, after) {
3256                (Some(before), Some(after)) => {
3257                    before.kind != after.kind
3258                        || before.metadata != after.metadata
3259                        || (before.kind != FileKind::Directory && before.payload != after.payload)
3260                }
3261                (None, None) => false,
3262                _ => true,
3263            })
3264            .map(|(path, (before, after))| ChangedPath {
3265                path,
3266                before,
3267                after,
3268            })
3269            .collect();
3270        Ok(crate::OperationReceipt { value: paths, work })
3271    }
3272
3273    /// Composes contiguous immutable deltas into their exact net semantic
3274    /// change. Intermediate changes that cancel are absent from the result.
3275    ///
3276    /// The implementation diffs the outer Merkle endpoints instead of
3277    /// concatenating records, preserving canonical rename, hard-link, revert,
3278    /// and equal-subtree behavior.
3279    ///
3280    /// # Errors
3281    ///
3282    /// Rejects non-contiguous or foreign deltas and bounded diff exhaustion.
3283    pub async fn compose(
3284        &self,
3285        next: &ChangeSet<A, O>,
3286        maximum_changes: u32,
3287    ) -> Result<ChangeSet<A, O>, WorkspaceError> {
3288        if self.to.id != next.from.id
3289            || self.to.workspace.id != next.from.workspace.id
3290            || !self
3291                .to
3292                .workspace
3293                .volume
3294                .fs
3295                .same_deployment(&next.from.workspace.volume.fs)
3296        {
3297            return Err(WorkspaceError::ChangeSetContinuity);
3298        }
3299        self.from
3300            .workspace
3301            .diff(&self.from, &next.to, maximum_changes)
3302            .await
3303    }
3304}
3305
3306/// How a successful join records immutable ancestry.
3307#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3308pub enum JoinHistory {
3309    /// Preserve source and target as two parents.
3310    Merge,
3311    /// Replay source state onto the target as one target-parent generation.
3312    Rebase,
3313    /// Collapse the complete source delta into one target-parent generation.
3314    Squash,
3315    /// Apply the selected source delta as one target-parent generation.
3316    CherryPick,
3317}
3318
3319/// Side-effect-free join builder.
3320pub struct JoinBuilder<A, O> {
3321    source: Workspace<A, O>,
3322    target: Workspace<A, O>,
3323    history: JoinHistory,
3324    maximum_generations: u32,
3325    maximum_changes: u32,
3326    maximum_conflicts: u32,
3327}
3328
3329impl<A, O> JoinBuilder<A, O> {
3330    fn validate_bounds(&self) -> Result<(), WorkspaceError> {
3331        if self.maximum_generations == 0 || self.maximum_changes == 0 || self.maximum_conflicts == 0
3332        {
3333            Err(WorkspaceError::JoinLimit)
3334        } else {
3335            Ok(())
3336        }
3337    }
3338
3339    /// Selects the immutable ancestry shape of a successful join.
3340    #[must_use]
3341    pub const fn history(mut self, history: JoinHistory) -> Self {
3342        self.history = history;
3343        self
3344    }
3345
3346    /// Sets exact bounded lineage, diff, and conflict frontiers.
3347    #[must_use]
3348    pub const fn bounds(
3349        mut self,
3350        maximum_generations: u32,
3351        maximum_changes: u32,
3352        maximum_conflicts: u32,
3353    ) -> Self {
3354        self.maximum_generations = maximum_generations;
3355        self.maximum_changes = maximum_changes;
3356        self.maximum_conflicts = maximum_conflicts;
3357        self
3358    }
3359}
3360
3361impl<A: AsyncAuthorityStore, O: AsyncObjectStore> JoinBuilder<A, O> {
3362    /// Authenticates endpoints and discovers one bounded common ancestor
3363    /// without writing immutable objects or changing either authority.
3364    ///
3365    /// # Errors
3366    ///
3367    /// Rejects unrelated deployments, incompatible semantics, missing or
3368    /// over-bound lineage, and malformed authenticated state.
3369    pub async fn plan(self) -> Result<JoinPlan<A, O>, WorkspaceError> {
3370        self.validate_bounds()?;
3371        let source_head = self.source.head().await?;
3372        let (target_head_id, target_authority_head) = self
3373            .target
3374            .volume
3375            .fs
3376            .workspace_head_state(&self.target.volume)
3377            .await?;
3378        let target_head = Generation {
3379            workspace: self.target.clone(),
3380            id: target_head_id,
3381        };
3382        self.plan_generations(source_head, target_head, target_authority_head)
3383            .await
3384    }
3385
3386    /// Plans against caller-pinned immutable source and target generations.
3387    ///
3388    /// The target must still be current when planning begins. The source may
3389    /// have advanced: publication deliberately retains the generation pinned
3390    /// when the parent began inspecting the child.
3391    pub async fn plan_pinned(
3392        self,
3393        source_head: Generation<A, O>,
3394        target_head: Generation<A, O>,
3395    ) -> Result<JoinPlan<A, O>, WorkspaceError> {
3396        self.validate_bounds()?;
3397        if source_head.workspace.id != self.source.id || target_head.workspace.id != self.target.id
3398        {
3399            return Err(WorkspaceError::ForeignGeneration);
3400        }
3401        let (current_target, target_authority_head) = self
3402            .target
3403            .volume
3404            .fs
3405            .workspace_head_state(&self.target.volume)
3406            .await?;
3407        if current_target != target_head.id {
3408            return Err(WorkspaceError::StaleGeneration);
3409        }
3410        self.plan_generations(source_head, target_head, target_authority_head)
3411            .await
3412    }
3413
3414    pub(crate) async fn plan_generations(
3415        self,
3416        source_head: Generation<A, O>,
3417        target_head: Generation<A, O>,
3418        target_authority_head: crate::Head,
3419    ) -> Result<JoinPlan<A, O>, WorkspaceError> {
3420        self.validate_bounds()?;
3421        if source_head.workspace.id != self.source.id || target_head.workspace.id != self.target.id
3422        {
3423            return Err(WorkspaceError::ForeignGeneration);
3424        }
3425        let base = self
3426            .source
3427            .volume
3428            .fs
3429            .workspace_common_ancestor(&source_head, &target_head, self.maximum_generations)
3430            .await?;
3431        Ok(JoinPlan {
3432            source: self.source,
3433            target: self.target,
3434            source_head,
3435            target_head,
3436            target_authority_head,
3437            base,
3438            history: self.history,
3439            maximum_generations: self.maximum_generations,
3440            maximum_changes: self.maximum_changes,
3441            maximum_conflicts: self.maximum_conflicts,
3442        })
3443    }
3444}
3445
3446/// Immutable side-effect-free join plan.
3447pub struct JoinPlan<A, O> {
3448    source: Workspace<A, O>,
3449    target: Workspace<A, O>,
3450    source_head: Generation<A, O>,
3451    target_head: Generation<A, O>,
3452    target_authority_head: crate::Head,
3453    base: crate::facade::WorkspaceCommonAncestor,
3454    history: JoinHistory,
3455    maximum_generations: u32,
3456    maximum_changes: u32,
3457    maximum_conflicts: u32,
3458}
3459
3460impl<A, O> JoinPlan<A, O> {
3461    /// Source generation captured while planning.
3462    #[must_use]
3463    pub const fn source_head(&self) -> GenerationId {
3464        self.source_head.id
3465    }
3466
3467    /// Target generation observed while planning.
3468    #[must_use]
3469    pub const fn target_head(&self) -> GenerationId {
3470        self.target_head.id
3471    }
3472
3473    /// Exact discovered common ancestor.
3474    #[must_use]
3475    pub const fn common_ancestor(&self) -> GenerationId {
3476        self.base.id
3477    }
3478
3479    /// Workspace volume that authenticated the exact common ancestor.
3480    #[must_use]
3481    #[cfg(not(target_arch = "wasm32"))]
3482    pub(crate) const fn common_ancestor_volume(&self) -> VolumeId {
3483        self.base.volume_id
3484    }
3485
3486    /// Source workspace retained by the plan.
3487    #[must_use]
3488    pub const fn source(&self) -> &Workspace<A, O> {
3489        &self.source
3490    }
3491}
3492
3493/// Atomic join application preconditions.
3494#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3495pub struct ApplyOptions {
3496    /// Exact target generation that must still be current.
3497    pub if_target: GenerationId,
3498    /// Stable identity reused only for an exact retry.
3499    pub idempotency_key: IdempotencyKey,
3500}
3501
3502/// Terminal semantic join outcome.
3503pub enum JoinOutcome<A, O> {
3504    /// A new target generation became durable.
3505    Applied(Generation<A, O>),
3506    /// The same exact application was already durable.
3507    AlreadyApplied(Generation<A, O>),
3508    /// Source changes were already represented by the target.
3509    NoChanges(Generation<A, O>),
3510    /// Target changed after planning; no join was published.
3511    StaleTarget(Generation<A, O>),
3512    /// Exact conflicts prevented candidate publication.
3513    Conflicted {
3514        /// Stable path-independent conflict regions.
3515        conflicts: Vec<MergeConflict>,
3516        /// Additional conflicts exceeded the retained result bound.
3517        truncated: bool,
3518    },
3519    /// Target writer ownership changed before publication.
3520    Fenced,
3521    /// Retry identity was previously bound to another join input.
3522    IdempotencyConflict,
3523}
3524
3525pub(crate) enum WorkspaceRestoreOutcome {
3526    Restored(GenerationId),
3527    AlreadyRestored(GenerationId),
3528    Current(GenerationId),
3529    Stale(GenerationId),
3530    Fenced,
3531    IdempotencyConflict,
3532}
3533
3534/// Terminal semantic outcome of an exact workspace-head restoration.
3535pub enum WorkspaceRestore<A, O> {
3536    /// The requested historical generation became current.
3537    Restored(Generation<A, O>),
3538    /// The same restoration was already durable under this retry identity.
3539    AlreadyRestored(Generation<A, O>),
3540    /// The requested generation was already current; no publication occurred.
3541    Current(Generation<A, O>),
3542    /// The workspace advanced after the caller observed it.
3543    Stale(Generation<A, O>),
3544    /// Workspace writer ownership changed before publication.
3545    Fenced,
3546    /// The retry identity was previously bound to another restoration.
3547    IdempotencyConflict,
3548}
3549
3550/// One exact path rejected by a three-way path application.
3551#[derive(Clone, Debug, Eq, PartialEq)]
3552pub struct WorkspacePathConflict {
3553    /// Portable path relative to the workspace root.
3554    pub path: String,
3555    /// Semantic category suitable for driver or UI selection.
3556    pub kind: ConflictKind,
3557}
3558
3559/// Terminal outcome of applying a generation delta to selected paths.
3560pub enum WorkspacePathApply<A, O> {
3561    /// A new generation became durable.
3562    Applied(Generation<A, O>),
3563    /// The exact idempotent application was already durable.
3564    AlreadyApplied(Generation<A, O>),
3565    /// The requested delta was already represented by the live workspace.
3566    NoChanges(Generation<A, O>),
3567    /// Independent live changes overlap the requested generation delta.
3568    Conflicted(Vec<WorkspacePathConflict>),
3569    /// The live workspace advanced after the caller's observation.
3570    Stale(Generation<A, O>),
3571    /// Workspace writer ownership changed before publication.
3572    Fenced,
3573    /// The retry identity was previously used for different inputs.
3574    IdempotencyConflict,
3575}
3576
3577/// Failure while resolving or validating a declarative real-workspace join.
3578#[derive(Debug, Error)]
3579pub enum DrivenJoinError {
3580    /// Workspace planning, storage, or publication failed.
3581    #[error(transparent)]
3582    Workspace(#[from] WorkspaceError),
3583    /// Driver selection, execution, or cache reuse failed.
3584    #[error(transparent)]
3585    Resolution(#[from] MergePlanResolutionError),
3586    /// Candidate generations or conflict identities do not match this join plan.
3587    #[error("merge candidate does not match the immutable workspace join plan")]
3588    StaleCandidate,
3589    /// The current kernel cannot safely materialize this synthesized resolution.
3590    #[error("merge resolution for {0:?} is not representable by this join")]
3591    UnsupportedResolution(ConflictKey),
3592}
3593
3594impl<A: AsyncAuthorityStore, O: AsyncObjectStore> JoinPlan<A, O> {
3595    #[cfg(not(target_arch = "wasm32"))]
3596    pub(crate) async fn source_changes(
3597        &self,
3598    ) -> Result<crate::FsReceipt<GenerationDiff>, WorkspaceError> {
3599        self.source
3600            .volume
3601            .fs
3602            .workspace_join_changes(self.base.id, &self.source_head, self.maximum_changes)
3603            .await
3604    }
3605
3606    /// Applies this immutable plan through one target-head CAS.
3607    ///
3608    /// # Errors
3609    ///
3610    /// Returns authenticated storage, compatibility, bound, or publication
3611    /// failures. Semantic conflicts and races are typed outcomes.
3612    pub async fn apply(&self, options: ApplyOptions) -> Result<JoinOutcome<A, O>, WorkspaceError> {
3613        self.apply_with_permit(options, crate::PublicationPermit::Unrestricted)
3614            .await
3615    }
3616
3617    /// Applies this immutable plan only while the supplied writer permit remains valid.
3618    pub async fn apply_with_permit(
3619        &self,
3620        options: ApplyOptions,
3621        permit: crate::PublicationPermit,
3622    ) -> Result<JoinOutcome<A, O>, WorkspaceError> {
3623        self.apply_resolutions(options, BTreeMap::new(), permit)
3624            .await
3625    }
3626
3627    /// Expands exact kernel conflicts into immutable, driver-ready values.
3628    ///
3629    /// This is side-effect free and lets adapters present the same typed paths
3630    /// and alternatives used by [`Self::apply_with_drivers`].
3631    pub async fn describe_conflicts(
3632        &self,
3633        conflicts: &[MergeConflict],
3634        truncated: bool,
3635    ) -> Result<MergePlan, WorkspaceError> {
3636        self.typed_merge_plan(conflicts.to_vec(), truncated).await
3637    }
3638
3639    /// Runs registered immutable-input drivers for real join conflicts and
3640    /// publishes the validated candidate through the ordinary target CAS.
3641    pub async fn apply_with_drivers<C: MergeResolutionCache>(
3642        &self,
3643        options: ApplyOptions,
3644        registry: &MergeDriverRegistry,
3645        cache: &mut C,
3646        replanning: bool,
3647    ) -> Result<JoinOutcome<A, O>, DrivenJoinError> {
3648        self.apply_with_drivers_and_permit(
3649            options,
3650            registry,
3651            cache,
3652            replanning,
3653            crate::PublicationPermit::Unrestricted,
3654        )
3655        .await
3656    }
3657
3658    /// Runs merge drivers and publishes only while the supplied writer permit remains valid.
3659    pub async fn apply_with_drivers_and_permit<C: MergeResolutionCache>(
3660        &self,
3661        options: ApplyOptions,
3662        registry: &MergeDriverRegistry,
3663        cache: &mut C,
3664        replanning: bool,
3665        permit: crate::PublicationPermit,
3666    ) -> Result<JoinOutcome<A, O>, DrivenJoinError> {
3667        let initial = self.apply_with_permit(options, permit).await?;
3668        let JoinOutcome::Conflicted {
3669            conflicts,
3670            truncated,
3671        } = initial
3672        else {
3673            return Ok(initial);
3674        };
3675        let plan = self.typed_merge_plan(conflicts, truncated).await?;
3676        let candidate = resolve_merge_plan(plan, registry, cache, replanning)?;
3677        self.apply_candidate_with_permit(options, &candidate, permit)
3678            .await
3679    }
3680
3681    /// Applies caller-selected immutable sides after validating that they
3682    /// exactly cover the conflicts produced by this join plan.
3683    pub async fn apply_sides(
3684        &self,
3685        options: ApplyOptions,
3686        selections: BTreeMap<MergeConflict, ConflictSide>,
3687    ) -> Result<JoinOutcome<A, O>, DrivenJoinError> {
3688        let initial = self.apply(options).await?;
3689        let JoinOutcome::Conflicted {
3690            conflicts,
3691            truncated,
3692        } = initial
3693        else {
3694            return Ok(initial);
3695        };
3696        if truncated
3697            || conflicts.len() != selections.len()
3698            || conflicts
3699                .iter()
3700                .any(|conflict| !selections.contains_key(conflict))
3701        {
3702            return Err(DrivenJoinError::StaleCandidate);
3703        }
3704        let resolutions = selections
3705            .into_iter()
3706            .map(|(conflict, side)| {
3707                let side = match side {
3708                    ConflictSide::Base => crate::kernel::MergeConflictSide::Base,
3709                    ConflictSide::Ours => crate::kernel::MergeConflictSide::Ours,
3710                    ConflictSide::Theirs => crate::kernel::MergeConflictSide::Theirs,
3711                };
3712                (
3713                    conflict,
3714                    crate::kernel::MergeConflictResolution::Select(side),
3715                )
3716            })
3717            .collect();
3718        self.apply_resolutions(options, resolutions, crate::PublicationPermit::Unrestricted)
3719            .await
3720            .map_err(Into::into)
3721    }
3722
3723    /// Validates caller-selected declarations against this exact immutable
3724    /// join and publishes them through the same fenced compare-and-swap path.
3725    pub async fn apply_candidate(
3726        &self,
3727        options: ApplyOptions,
3728        candidate: &UnpublishedMergeCandidate,
3729    ) -> Result<JoinOutcome<A, O>, DrivenJoinError> {
3730        self.apply_candidate_with_permit(options, candidate, crate::PublicationPermit::Unrestricted)
3731            .await
3732    }
3733
3734    /// Validates and publishes a declarative candidate under one writer permit.
3735    pub async fn apply_candidate_with_permit(
3736        &self,
3737        options: ApplyOptions,
3738        candidate: &UnpublishedMergeCandidate,
3739        permit: crate::PublicationPermit,
3740    ) -> Result<JoinOutcome<A, O>, DrivenJoinError> {
3741        let conflict_keys = candidate
3742            .plan
3743            .conflicts
3744            .iter()
3745            .map(|conflict| &conflict.key)
3746            .collect::<BTreeSet<_>>();
3747        if candidate.plan.base != self.base.id
3748            || candidate.plan.ours != self.target_head.id
3749            || candidate.plan.theirs != self.source_head.id
3750            || candidate.plan.truncated
3751            || candidate.plan.conflicts.len() != candidate.resolutions.len()
3752            || conflict_keys.len() != candidate.plan.conflicts.len()
3753            || !candidate.resolutions.keys().eq(conflict_keys.into_iter())
3754        {
3755            return Err(DrivenJoinError::StaleCandidate);
3756        }
3757        let mut resolutions = BTreeMap::new();
3758        for conflict in &candidate.plan.conflicts {
3759            let resolution = candidate
3760                .resolutions
3761                .get(&conflict.key)
3762                .ok_or(DrivenJoinError::StaleCandidate)?;
3763            if !resolution_matches_conflict(&conflict.key, resolution) {
3764                return Err(DrivenJoinError::UnsupportedResolution(conflict.key.clone()));
3765            }
3766            let resolution = match resolution {
3767                MergeResolution::Select(ConflictSide::Base) => {
3768                    crate::kernel::MergeConflictResolution::Select(
3769                        crate::kernel::MergeConflictSide::Base,
3770                    )
3771                }
3772                MergeResolution::Select(ConflictSide::Ours) => {
3773                    crate::kernel::MergeConflictResolution::Select(
3774                        crate::kernel::MergeConflictSide::Ours,
3775                    )
3776                }
3777                MergeResolution::Select(ConflictSide::Theirs) => {
3778                    crate::kernel::MergeConflictResolution::Select(
3779                        crate::kernel::MergeConflictSide::Theirs,
3780                    )
3781                }
3782                MergeResolution::Text(text) => crate::kernel::MergeConflictResolution::File(Some(
3783                    self.stage_regular_resolution(
3784                        &conflict.key,
3785                        Bytes::copy_from_slice(text.as_bytes()),
3786                    )
3787                    .await?,
3788                )),
3789                MergeResolution::Binary(bytes) => {
3790                    crate::kernel::MergeConflictResolution::File(Some(
3791                        self.stage_regular_resolution(&conflict.key, Bytes::copy_from_slice(bytes))
3792                            .await?,
3793                    ))
3794                }
3795                MergeResolution::Metadata(bytes) => {
3796                    let file_id = conflict_file_id(&conflict.key).ok_or_else(|| {
3797                        DrivenJoinError::UnsupportedResolution(conflict.key.clone())
3798                    })?;
3799                    let mut record = self
3800                        .target
3801                        .volume
3802                        .fs
3803                        .workspace_conflict_record(
3804                            &self.target.volume,
3805                            self.target_head.id,
3806                            file_id,
3807                        )
3808                        .await?
3809                        .ok_or(WorkspaceError::InvalidMergeResolution)?;
3810                    record.metadata = self
3811                        .target
3812                        .volume
3813                        .fs
3814                        .stage_merge_metadata(&self.target.volume, bytes)
3815                        .await?;
3816                    crate::kernel::MergeConflictResolution::File(Some(record))
3817                }
3818                MergeResolution::Binding(file_id) => {
3819                    crate::kernel::MergeConflictResolution::Binding(*file_id)
3820                }
3821                MergeResolution::Unresolved => {
3822                    return Err(DrivenJoinError::UnsupportedResolution(conflict.key.clone()));
3823                }
3824            };
3825            resolutions.insert(
3826                kernel_conflict(
3827                    &conflict.key,
3828                    self.target.volume.config.limits.maximum_component_bytes,
3829                )?,
3830                resolution,
3831            );
3832        }
3833        self.apply_resolutions(options, resolutions, permit)
3834            .await
3835            .map_err(Into::into)
3836    }
3837
3838    async fn stage_regular_resolution(
3839        &self,
3840        key: &ConflictKey,
3841        bytes: Bytes,
3842    ) -> Result<crate::kernel::FileRecord, DrivenJoinError> {
3843        let file_id = conflict_file_id(key)
3844            .ok_or_else(|| DrivenJoinError::UnsupportedResolution(key.clone()))?;
3845        let record = self
3846            .target
3847            .volume
3848            .fs
3849            .workspace_conflict_record(&self.target.volume, self.target_head.id, file_id)
3850            .await?
3851            .ok_or(WorkspaceError::InvalidMergeResolution)?;
3852        self.target
3853            .volume
3854            .fs
3855            .stage_merge_regular_record(&self.target.volume, record, bytes)
3856            .await
3857            .map_err(Into::into)
3858    }
3859
3860    async fn typed_merge_plan(
3861        &self,
3862        conflicts: Vec<MergeConflict>,
3863        truncated: bool,
3864    ) -> Result<MergePlan, WorkspaceError> {
3865        let mut typed = Vec::with_capacity(conflicts.len());
3866        for conflict in conflicts {
3867            typed.push(self.conflict_view(conflict).await?);
3868        }
3869        Ok(MergePlan {
3870            base: self.base.id,
3871            ours: self.target_head.id,
3872            theirs: self.source_head.id,
3873            conflicts: typed,
3874            truncated,
3875        })
3876    }
3877
3878    async fn conflict_view(
3879        &self,
3880        conflict: MergeConflict,
3881    ) -> Result<crate::ConflictView, WorkspaceError> {
3882        let MergeConflict::File(file_id) = conflict else {
3883            let mut view = binding_conflict_view(self, conflict.clone());
3884            if let MergeConflict::Binding { directory_id, name } = conflict
3885                && let Some(directory) = self.conflict_path(directory_id).await?
3886                && let Ok(name) = logical_name_text(&name)
3887            {
3888                view.path = Some(if directory == "/" {
3889                    format!("/{name}")
3890                } else {
3891                    format!("{directory}/{name}")
3892                });
3893            }
3894            return Ok(view);
3895        };
3896        let path = self.conflict_path(file_id).await?;
3897        let fs = &self.target.volume.fs;
3898        let base = fs
3899            .workspace_conflict_record(&self.target.volume, self.base.id, file_id)
3900            .await?;
3901        let ours = fs
3902            .workspace_conflict_record(&self.target.volume, self.target_head.id, file_id)
3903            .await?;
3904        let theirs = fs
3905            .workspace_conflict_record(&self.target.volume, self.source_head.id, file_id)
3906            .await?;
3907        let fallback = |generation, record: Option<crate::kernel::FileRecord>| match record {
3908            Some(_) => ConflictValue::Record {
3909                generation,
3910                file_id,
3911            },
3912            None => ConflictValue::Absent,
3913        };
3914        let Some((base_record, ours_record, theirs_record)) = base
3915            .zip(ours)
3916            .zip(theirs)
3917            .map(|((base, ours), theirs)| (base, ours, theirs))
3918        else {
3919            return Ok(crate::ConflictView {
3920                key: ConflictKey::File(file_id),
3921                path,
3922                kind: ConflictKind::Record,
3923                base: fallback(self.base.id, base),
3924                ours: fallback(self.target_head.id, ours),
3925                theirs: fallback(self.source_head.id, theirs),
3926            });
3927        };
3928        if base_record.link_count != ours_record.link_count
3929            || base_record.link_count != theirs_record.link_count
3930        {
3931            return Ok(record_conflict_view(
3932                self,
3933                file_id,
3934                path,
3935                ConflictKind::HardLink,
3936            ));
3937        }
3938        if base_record.metadata != ours_record.metadata
3939            || base_record.metadata != theirs_record.metadata
3940        {
3941            let values = [base_record, ours_record, theirs_record];
3942            let mut metadata = Vec::with_capacity(3);
3943            for record in values {
3944                metadata.push(ConflictValue::Metadata(
3945                    fs.workspace_conflict_metadata(&self.target.volume, record)
3946                        .await?,
3947                ));
3948            }
3949            return Ok(crate::ConflictView {
3950                key: ConflictKey::Metadata(file_id),
3951                path,
3952                kind: ConflictKind::Metadata,
3953                base: metadata.remove(0),
3954                ours: metadata.remove(0),
3955                theirs: metadata.remove(0),
3956            });
3957        }
3958        if let Some(view) = self
3959            .regular_conflict_view(
3960                file_id,
3961                path.clone(),
3962                [base_record, ours_record, theirs_record],
3963            )
3964            .await?
3965        {
3966            return Ok(view);
3967        }
3968        let kind = match base_record.kind {
3969            FileKind::Directory => ConflictKind::Directory,
3970            FileKind::SymbolicLink => ConflictKind::SymbolicLink,
3971            _ => ConflictKind::Special,
3972        };
3973        Ok(record_conflict_view(self, file_id, path, kind))
3974    }
3975
3976    async fn conflict_path(&self, file_id: FileId) -> Result<Option<String>, WorkspaceError> {
3977        let maximum = self.maximum_changes.max(1);
3978        let mut paths = self.target_head.paths_for_file_id(file_id, maximum).await?;
3979        if paths.is_empty() {
3980            paths = self.source_head.paths_for_file_id(file_id, maximum).await?;
3981        }
3982        Ok(paths.into_iter().next())
3983    }
3984
3985    async fn regular_conflict_view(
3986        &self,
3987        file_id: FileId,
3988        path: Option<String>,
3989        records: [crate::kernel::FileRecord; 3],
3990    ) -> Result<Option<crate::ConflictView>, WorkspaceError> {
3991        if records
3992            .iter()
3993            .any(|record| record.kind != FileKind::Regular)
3994        {
3995            return Ok(None);
3996        }
3997        let mut bodies = Vec::with_capacity(3);
3998        for record in records {
3999            let Some(bytes) = self
4000                .target
4001                .volume
4002                .fs
4003                .workspace_conflict_bytes(&self.target.volume, record)
4004                .await?
4005            else {
4006                return Ok(Some(record_conflict_view(
4007                    self,
4008                    file_id,
4009                    path,
4010                    ConflictKind::Binary,
4011                )));
4012            };
4013            bodies.push(bytes);
4014        }
4015        let text = bodies
4016            .iter()
4017            .all(|bytes| std::str::from_utf8(bytes).is_ok());
4018        let mut values = bodies.into_iter().map(|bytes| {
4019            if text {
4020                ConflictValue::Text(String::from_utf8(bytes.to_vec()).unwrap_or_default())
4021            } else {
4022                ConflictValue::Binary(bytes.to_vec())
4023            }
4024        });
4025        Ok(Some(crate::ConflictView {
4026            key: ConflictKey::File(file_id),
4027            path,
4028            kind: if text {
4029                ConflictKind::Text
4030            } else {
4031                ConflictKind::Binary
4032            },
4033            base: values
4034                .next()
4035                .ok_or(WorkspaceError::InvalidMergeResolution)?,
4036            ours: values
4037                .next()
4038                .ok_or(WorkspaceError::InvalidMergeResolution)?,
4039            theirs: values
4040                .next()
4041                .ok_or(WorkspaceError::InvalidMergeResolution)?,
4042        }))
4043    }
4044
4045    async fn apply_resolutions(
4046        &self,
4047        options: ApplyOptions,
4048        resolutions: BTreeMap<MergeConflict, crate::kernel::MergeConflictResolution>,
4049        permit: crate::PublicationPermit,
4050    ) -> Result<JoinOutcome<A, O>, WorkspaceError> {
4051        if options.if_target != self.target_head.id {
4052            return Ok(JoinOutcome::StaleTarget(self.target.head().await?));
4053        }
4054        let outcome = self
4055            .target
4056            .volume
4057            .fs
4058            .apply_workspace_join(crate::facade::WorkspaceJoinRequest {
4059                target: &self.target.volume,
4060                base: self.base,
4061                source: &self.source_head,
4062                expected_target: options.if_target,
4063                expected_head: self.target_authority_head,
4064                history: self.history,
4065                operation_id: options.idempotency_key.operation_id(),
4066                permit,
4067                maximum_generations: self.maximum_generations,
4068                maximum_changes: self.maximum_changes,
4069                maximum_conflicts: self.maximum_conflicts,
4070                resolutions,
4071            })
4072            .await?;
4073        let generation = |id| Generation {
4074            workspace: self.target.clone(),
4075            id,
4076        };
4077        Ok(match outcome {
4078            crate::facade::WorkspaceJoinOutcome::Applied(id) => {
4079                JoinOutcome::Applied(generation(id))
4080            }
4081            crate::facade::WorkspaceJoinOutcome::AlreadyApplied(id) => {
4082                JoinOutcome::AlreadyApplied(generation(id))
4083            }
4084            crate::facade::WorkspaceJoinOutcome::NoChanges(id) => {
4085                JoinOutcome::NoChanges(generation(id))
4086            }
4087            crate::facade::WorkspaceJoinOutcome::Stale(id) => {
4088                JoinOutcome::StaleTarget(generation(id))
4089            }
4090            crate::facade::WorkspaceJoinOutcome::Conflicted(conflicts, truncated) => {
4091                JoinOutcome::Conflicted {
4092                    conflicts,
4093                    truncated,
4094                }
4095            }
4096            crate::facade::WorkspaceJoinOutcome::Fenced => JoinOutcome::Fenced,
4097            crate::facade::WorkspaceJoinOutcome::IdempotencyConflict => {
4098                JoinOutcome::IdempotencyConflict
4099            }
4100        })
4101    }
4102}
4103
4104fn binding_conflict_view<A, O>(
4105    plan: &JoinPlan<A, O>,
4106    conflict: MergeConflict,
4107) -> crate::ConflictView {
4108    match conflict {
4109        MergeConflict::File(file_id) => crate::ConflictView {
4110            key: ConflictKey::File(file_id),
4111            path: None,
4112            kind: ConflictKind::Record,
4113            base: ConflictValue::RecordReference {
4114                generation: plan.base.id,
4115                file_id,
4116            },
4117            ours: ConflictValue::RecordReference {
4118                generation: plan.target_head.id,
4119                file_id,
4120            },
4121            theirs: ConflictValue::RecordReference {
4122                generation: plan.source_head.id,
4123                file_id,
4124            },
4125        },
4126        MergeConflict::Binding { directory_id, name } => {
4127            let name = canonical_name_key(&name);
4128            let value = |generation| ConflictValue::BindingReference {
4129                generation,
4130                directory_id,
4131                name: name.clone(),
4132            };
4133            crate::ConflictView {
4134                key: ConflictKey::Binding {
4135                    directory_id,
4136                    name: name.clone(),
4137                },
4138                path: None,
4139                kind: ConflictKind::Binding,
4140                base: value(plan.base.id),
4141                ours: value(plan.target_head.id),
4142                theirs: value(plan.source_head.id),
4143            }
4144        }
4145    }
4146}
4147
4148fn record_conflict_view<A, O>(
4149    plan: &JoinPlan<A, O>,
4150    file_id: FileId,
4151    path: Option<String>,
4152    kind: ConflictKind,
4153) -> crate::ConflictView {
4154    let value = |generation| ConflictValue::Record {
4155        generation,
4156        file_id,
4157    };
4158    crate::ConflictView {
4159        key: ConflictKey::File(file_id),
4160        path,
4161        kind,
4162        base: value(plan.base.id),
4163        ours: value(plan.target_head.id),
4164        theirs: value(plan.source_head.id),
4165    }
4166}
4167
4168fn canonical_name_key(name: &LogicalName) -> Vec<u8> {
4169    let mut key = Vec::with_capacity(name.as_bytes().len().saturating_add(1));
4170    key.push(match name.encoding() {
4171        crate::kernel::NameEncoding::Utf8 => 1,
4172        crate::kernel::NameEncoding::PosixBytes => 2,
4173        crate::kernel::NameEncoding::WindowsUtf16Le => 3,
4174    });
4175    key.extend_from_slice(name.as_bytes());
4176    key
4177}
4178
4179fn logical_name_text(name: &LogicalName) -> Result<std::borrow::Cow<'_, str>, WorkspaceError> {
4180    name.unicode_text().ok_or_else(|| {
4181        WorkspaceError::path("non-Unicode path cannot be projected as a portable string")
4182    })
4183}
4184
4185fn namespace_path_text(path: &NamespacePath) -> Result<String, WorkspaceError> {
4186    if path.is_root() {
4187        return Ok("/".to_owned());
4188    }
4189    let components = path
4190        .components()
4191        .iter()
4192        .map(logical_name_text)
4193        .collect::<Result<Vec<_>, _>>()?;
4194    Ok(format!("/{}", components.join("/")))
4195}
4196
4197fn child_namespace_path(
4198    directory: &NamespacePath,
4199    name: LogicalName,
4200    limits: crate::model::VolumeLimits,
4201) -> Result<NamespacePath, WorkspaceError> {
4202    let mut components = directory.components().to_vec();
4203    components.push(name);
4204    NamespacePath::new(components, limits).map_err(WorkspaceError::path)
4205}
4206
4207fn kernel_conflict(
4208    key: &ConflictKey,
4209    maximum_component_bytes: u32,
4210) -> Result<MergeConflict, DrivenJoinError> {
4211    match key {
4212        ConflictKey::File(file_id)
4213        | ConflictKey::Metadata(file_id)
4214        | ConflictKey::HardLinks(file_id)
4215        | ConflictKey::Directory(file_id)
4216        | ConflictKey::SpecialPayload(file_id)
4217        | ConflictKey::ContentRange { file_id, .. }
4218        | ConflictKey::Rename { file_id, .. } => Ok(MergeConflict::File(*file_id)),
4219        ConflictKey::Binding { directory_id, name } => {
4220            let (&tag, bytes) = name.split_first().ok_or(DrivenJoinError::StaleCandidate)?;
4221            let encoding = match tag {
4222                1 => crate::kernel::NameEncoding::Utf8,
4223                2 => crate::kernel::NameEncoding::PosixBytes,
4224                3 => crate::kernel::NameEncoding::WindowsUtf16Le,
4225                _ => return Err(DrivenJoinError::StaleCandidate),
4226            };
4227            let name = LogicalName::new(encoding, bytes.to_vec(), maximum_component_bytes)
4228                .map_err(|_| DrivenJoinError::StaleCandidate)?;
4229            Ok(MergeConflict::Binding {
4230                directory_id: *directory_id,
4231                name,
4232            })
4233        }
4234    }
4235}
4236
4237fn conflict_file_id(key: &ConflictKey) -> Option<FileId> {
4238    match key {
4239        ConflictKey::File(file_id)
4240        | ConflictKey::Metadata(file_id)
4241        | ConflictKey::HardLinks(file_id)
4242        | ConflictKey::Directory(file_id)
4243        | ConflictKey::SpecialPayload(file_id)
4244        | ConflictKey::ContentRange { file_id, .. }
4245        | ConflictKey::Rename { file_id, .. } => Some(*file_id),
4246        ConflictKey::Binding { .. } => None,
4247    }
4248}
4249
4250fn resolution_matches_conflict(key: &ConflictKey, resolution: &MergeResolution) -> bool {
4251    match resolution {
4252        MergeResolution::Select(_) | MergeResolution::Unresolved => true,
4253        MergeResolution::Text(_) | MergeResolution::Binary(_) => {
4254            matches!(key, ConflictKey::File(_) | ConflictKey::ContentRange { .. })
4255        }
4256        MergeResolution::Metadata(_) => matches!(key, ConflictKey::Metadata(_)),
4257        MergeResolution::Binding(_) => matches!(key, ConflictKey::Binding { .. }),
4258    }
4259}
4260
4261#[cfg(test)]
4262mod merge_resolution_tests {
4263    use super::*;
4264
4265    #[test]
4266    fn synthesized_resolutions_match_only_their_exact_conflict_shape() {
4267        let file_id = FileId::new();
4268        let file = ConflictKey::File(file_id);
4269        let metadata = ConflictKey::Metadata(file_id);
4270        let binding = ConflictKey::Binding {
4271            directory_id: file_id,
4272            name: vec![1, b'x'],
4273        };
4274
4275        for resolution in [
4276            MergeResolution::Text("merged".into()),
4277            MergeResolution::Binary(vec![1]),
4278        ] {
4279            assert!(resolution_matches_conflict(&file, &resolution));
4280            assert!(!resolution_matches_conflict(&metadata, &resolution));
4281            assert!(!resolution_matches_conflict(&binding, &resolution));
4282        }
4283        let metadata_resolution = MergeResolution::Metadata(vec![1]);
4284        assert!(resolution_matches_conflict(&metadata, &metadata_resolution));
4285        assert!(!resolution_matches_conflict(&file, &metadata_resolution));
4286        let binding_resolution = MergeResolution::Binding(Some(file_id));
4287        assert!(resolution_matches_conflict(&binding, &binding_resolution));
4288        assert!(!resolution_matches_conflict(&file, &binding_resolution));
4289        assert!(resolution_matches_conflict(
4290            &file,
4291            &MergeResolution::Select(ConflictSide::Ours)
4292        ));
4293    }
4294}
4295
4296impl<A, O> GenerationPin<A, O> {
4297    /// Canonical opaque pin identity.
4298    #[must_use]
4299    pub fn identity(&self) -> &WorkspaceName {
4300        &self.identity
4301    }
4302
4303    /// Exact retained immutable generation.
4304    #[must_use]
4305    pub const fn generation(&self) -> &Generation<A, O> {
4306        &self.generation
4307    }
4308}
4309
4310impl<A, O> Checkpoint<A, O> {
4311    /// Canonical retained label.
4312    #[must_use]
4313    pub fn label(&self) -> &WorkspaceName {
4314        &self.label
4315    }
4316
4317    /// Exact retained immutable generation.
4318    #[must_use]
4319    pub const fn generation(&self) -> &Generation<A, O> {
4320        &self.generation
4321    }
4322}
4323
4324impl<A, O> Clone for Generation<A, O> {
4325    fn clone(&self) -> Self {
4326        Self {
4327            workspace: self.workspace.clone(),
4328            id: self.id,
4329        }
4330    }
4331}
4332
4333impl<A, O> Generation<A, O> {
4334    /// Content-addressed generation identity.
4335    #[must_use]
4336    pub const fn id(&self) -> GenerationId {
4337        self.id
4338    }
4339
4340    /// Owning workspace identity.
4341    #[must_use]
4342    pub const fn workspace_id(&self) -> WorkspaceId {
4343        self.workspace.id
4344    }
4345
4346    /// Returns the workspace capability that owns this immutable generation.
4347    ///
4348    /// The returned handle is cheap and retains the same authenticated storage
4349    /// deployment. It is useful for composing generation-oriented algorithms
4350    /// without an adapter-owned workspace registry.
4351    #[must_use]
4352    pub fn workspace(&self) -> Workspace<A, O> {
4353        self.workspace.clone()
4354    }
4355}
4356
4357impl<A: AsyncAuthorityStore, O: AsyncObjectStore> Generation<A, O> {
4358    /// Opens one cheap immutable reader pinned to this exact generation.
4359    ///
4360    /// The generation root is authenticated once while the reader is opened;
4361    /// descendant objects remain lazy and are fetched only by the operation
4362    /// that needs them. Clone the returned reader to overlap independent
4363    /// requests without reopening the generation.
4364    ///
4365    /// # Errors
4366    ///
4367    /// Rejects unavailable, foreign, or corrupt generation state and bounded
4368    /// backend failures.
4369    pub async fn reader(&self) -> Result<crate::PinnedReader<A, O>, WorkspaceError> {
4370        self.workspace
4371            .engine_checkout(
4372                GenerationSelector::Exact(self.id),
4373                CheckoutMode::read_only_pinned(),
4374            )
4375            .await?
4376            .pinned_reader()
4377            .map_err(WorkspaceError::engine)
4378    }
4379
4380    /// Computes one semantic delta to a compatible generation in any workspace
4381    /// from the same filesystem deployment.
4382    pub async fn diff_to(
4383        &self,
4384        to: &Generation<A, O>,
4385        maximum_changes: u32,
4386    ) -> Result<ChangeSet<A, O>, WorkspaceError> {
4387        self.diff_to_bounded(
4388            to,
4389            maximum_changes,
4390            crate::WorkBudget::UNBOUNDED,
4391            &crate::CancellationToken::new(),
4392        )
4393        .await
4394    }
4395
4396    /// Computes one semantic delta with explicit cumulative work and
4397    /// cancellation bounds.
4398    pub async fn diff_to_bounded(
4399        &self,
4400        to: &Generation<A, O>,
4401        maximum_changes: u32,
4402        budget: crate::WorkBudget,
4403        cancellation: &crate::CancellationToken,
4404    ) -> Result<ChangeSet<A, O>, WorkspaceError> {
4405        let receipt = self
4406            .workspace
4407            .volume
4408            .fs
4409            .workspace_join_changes_bounded(self.id, to, maximum_changes, budget, cancellation)
4410            .await?;
4411        Ok(ChangeSet {
4412            from: self.clone(),
4413            to: to.clone(),
4414            changes: receipt.value,
4415            work: receipt.work,
4416        })
4417    }
4418
4419    /// Materializes this immutable generation into an existing empty host
4420    /// directory using the SDK's native capability-rooted adapter. Unix ctime
4421    /// and Linux birth time are host-generated view-local facts; their exact
4422    /// canonical values remain in this generation, not in the host inode.
4423    #[cfg(all(feature = "native-mount", not(target_arch = "wasm32")))]
4424    pub async fn materialize(
4425        &self,
4426        options: &crate::MaterializeOptions,
4427        budget: crate::WorkBudget,
4428        cancellation: &crate::CancellationToken,
4429    ) -> Result<crate::OperationReceipt<crate::MaterializationReceipt>, WorkspaceError> {
4430        self.materialize_with_mode(
4431            options,
4432            budget,
4433            cancellation,
4434            crate::native_mount::MaterializeMode::DurableOutput,
4435        )
4436        .await
4437    }
4438
4439    #[cfg(all(feature = "native-mount", not(target_arch = "wasm32")))]
4440    pub(crate) async fn materialize_with_mode(
4441        &self,
4442        options: &crate::MaterializeOptions,
4443        budget: crate::WorkBudget,
4444        cancellation: &crate::CancellationToken,
4445        mode: crate::native_mount::MaterializeMode,
4446    ) -> Result<crate::OperationReceipt<crate::MaterializationReceipt>, WorkspaceError> {
4447        let checkout = self
4448            .workspace
4449            .engine_checkout_measured(
4450                GenerationSelector::Exact(self.id),
4451                CheckoutMode::read_only_pinned(),
4452                budget,
4453                cancellation,
4454            )
4455            .await?;
4456        let mut value = checkout.value;
4457        let receipt = crate::native_mount::materialize_checkout_with_mode(
4458            &mut value,
4459            options,
4460            checkout
4461                .work
4462                .remaining(budget)
4463                .map_err(WorkspaceError::from)?,
4464            cancellation,
4465            mode,
4466        )
4467        .await
4468        .map_err(|failure| WorkspaceError::engine(failure.error))?;
4469        merge_workspace_work(checkout.work, receipt, budget)
4470    }
4471
4472    /// Materializes one path from this generation below an existing empty
4473    /// host directory. The same relative path is reproduced below the
4474    /// destination, allowing callers to stage and atomically exchange it.
4475    #[cfg(all(feature = "native-mount", not(target_arch = "wasm32")))]
4476    pub async fn materialize_path(
4477        &self,
4478        path: &str,
4479        options: &crate::MaterializeOptions,
4480        budget: crate::WorkBudget,
4481        cancellation: &crate::CancellationToken,
4482    ) -> Result<crate::OperationReceipt<crate::MaterializationReceipt>, WorkspaceError> {
4483        self.materialize_path_with_mode(
4484            path,
4485            options,
4486            budget,
4487            cancellation,
4488            crate::native_mount::MaterializeMode::DurableOutput,
4489        )
4490        .await
4491    }
4492
4493    #[cfg(all(feature = "native-mount", not(target_arch = "wasm32")))]
4494    pub(crate) async fn materialize_path_with_mode(
4495        &self,
4496        path: &str,
4497        options: &crate::MaterializeOptions,
4498        budget: crate::WorkBudget,
4499        cancellation: &crate::CancellationToken,
4500        mode: crate::native_mount::MaterializeMode,
4501    ) -> Result<crate::OperationReceipt<crate::MaterializationReceipt>, WorkspaceError> {
4502        let checkout = self
4503            .workspace
4504            .engine_checkout_measured(
4505                GenerationSelector::Exact(self.id),
4506                CheckoutMode::read_only_pinned(),
4507                budget,
4508                cancellation,
4509            )
4510            .await?;
4511        let mut value = checkout.value;
4512        let path = customer_path(path, value.volume_config())?;
4513        let receipt = crate::native_mount::materialize_checkout_paths_with_mode(
4514            &mut value,
4515            &[path],
4516            options,
4517            checkout
4518                .work
4519                .remaining(budget)
4520                .map_err(WorkspaceError::from)?,
4521            cancellation,
4522            mode,
4523        )
4524        .await
4525        .map_err(|failure| WorkspaceError::engine(failure.error))?;
4526        merge_workspace_work(checkout.work, receipt, budget)
4527    }
4528
4529    /// Materializes multiple paths with one pinned checkout and shared
4530    /// file-identity table, preserving hard links across selected paths.
4531    #[cfg(all(feature = "native-mount", not(target_arch = "wasm32")))]
4532    pub async fn materialize_paths(
4533        &self,
4534        paths: &[String],
4535        options: &crate::MaterializeOptions,
4536        budget: crate::WorkBudget,
4537        cancellation: &crate::CancellationToken,
4538    ) -> Result<crate::OperationReceipt<crate::MaterializationReceipt>, WorkspaceError> {
4539        let checkout = self
4540            .workspace
4541            .engine_checkout_measured(
4542                GenerationSelector::Exact(self.id),
4543                CheckoutMode::read_only_pinned(),
4544                budget,
4545                cancellation,
4546            )
4547            .await?;
4548        let mut value = checkout.value;
4549        let paths = paths
4550            .iter()
4551            .map(|path| customer_path(path, value.volume_config()))
4552            .collect::<Result<Vec<_>, _>>()?;
4553        let receipt = crate::materialize_checkout_paths(
4554            &mut value,
4555            &paths,
4556            options,
4557            checkout
4558                .work
4559                .remaining(budget)
4560                .map_err(WorkspaceError::from)?,
4561            cancellation,
4562        )
4563        .await
4564        .map_err(|failure| WorkspaceError::engine(failure.error))?;
4565        merge_workspace_work(checkout.work, receipt, budget)
4566    }
4567
4568    /// Restores one path from this immutable generation into a host tree.
4569    ///
4570    /// Only the selected path is read and touched. Publication is same-volume
4571    /// and atomic for an ordinary host tree, or mount-visible for a live mount.
4572    #[cfg(all(feature = "native-mount", not(target_arch = "wasm32")))]
4573    pub async fn restore_host_path(
4574        &self,
4575        relative: &std::path::Path,
4576        replacement: crate::HostPathReplacement,
4577        options: &crate::MaterializeOptions,
4578        budget: crate::WorkBudget,
4579        cancellation: &crate::CancellationToken,
4580    ) -> Result<crate::OperationReceipt<crate::HostPathRestore>, WorkspaceError> {
4581        let checkout = self
4582            .workspace
4583            .engine_checkout_measured(
4584                GenerationSelector::Exact(self.id),
4585                CheckoutMode::read_only_pinned(),
4586                budget,
4587                cancellation,
4588            )
4589            .await?;
4590        let mut value = checkout.value;
4591        let receipt = crate::restore_checkout_host_path(
4592            &mut value,
4593            relative,
4594            replacement,
4595            options,
4596            checkout
4597                .work
4598                .remaining(budget)
4599                .map_err(WorkspaceError::from)?,
4600            cancellation,
4601        )
4602        .await
4603        .map_err(|failure| WorkspaceError::engine(failure.error))?;
4604        merge_workspace_work(checkout.work, receipt, budget)
4605    }
4606
4607    /// Restores a bounded sequence of paths through one pinned checkout.
4608    #[cfg(all(feature = "native-mount", not(target_arch = "wasm32")))]
4609    pub async fn restore_host_paths(
4610        &self,
4611        paths: &[std::path::PathBuf],
4612        replacement: crate::HostPathReplacement,
4613        options: &crate::MaterializeOptions,
4614        cancellation: &crate::CancellationToken,
4615    ) -> Result<Vec<crate::HostPathRestore>, WorkspaceError> {
4616        let mut checkout = self
4617            .workspace
4618            .engine_checkout_measured(
4619                GenerationSelector::Exact(self.id),
4620                CheckoutMode::read_only_pinned(),
4621                crate::WorkBudget::UNBOUNDED,
4622                cancellation,
4623            )
4624            .await?
4625            .value;
4626        let mut restored = Vec::new();
4627        restored
4628            .try_reserve_exact(paths.len())
4629            .map_err(|_| WorkspaceError::engine("restore path result allocation failed"))?;
4630        for path in paths {
4631            restored.push(
4632                crate::restore_checkout_host_path(
4633                    &mut checkout,
4634                    path,
4635                    replacement,
4636                    options,
4637                    crate::WorkBudget::UNBOUNDED,
4638                    cancellation,
4639                )
4640                .await
4641                .map_err(|failure| WorkspaceError::engine(failure.error))?
4642                .value,
4643            );
4644        }
4645        Ok(restored)
4646    }
4647
4648    /// Returns the exact immutable parent generation identities.
4649    ///
4650    /// # Errors
4651    ///
4652    /// Returns an authenticated storage or identity failure for malformed state.
4653    pub async fn parents(&self) -> Result<Vec<GenerationId>, WorkspaceError> {
4654        self.workspace
4655            .volume
4656            .fs
4657            .generation_parents(&self.workspace.volume, self.id)
4658            .await
4659    }
4660
4661    /// Reads at most `maximum_bytes` from one complete file in this exact
4662    /// immutable generation.
4663    ///
4664    /// # Errors
4665    ///
4666    /// Rejects invalid, absent, non-file, oversized, or unauthenticated state.
4667    pub async fn read(&self, path: &str, maximum_bytes: u64) -> Result<Bytes, WorkspaceError> {
4668        read_generation(
4669            &self.workspace,
4670            GenerationSelector::Exact(self.id),
4671            path,
4672            maximum_bytes,
4673        )
4674        .await
4675    }
4676
4677    /// Reads one exact range from this immutable generation.
4678    ///
4679    /// # Errors
4680    ///
4681    /// Rejects invalid/absent/non-file paths, invalid ranges, and authenticated
4682    /// backend failures.
4683    pub async fn read_range(
4684        &self,
4685        path: &str,
4686        offset: u64,
4687        length: u64,
4688    ) -> Result<Bytes, WorkspaceError> {
4689        read_generation_range(
4690            &self.workspace,
4691            GenerationSelector::Exact(self.id),
4692            path,
4693            offset,
4694            length,
4695        )
4696        .await
4697    }
4698
4699    /// Returns complete metadata for one path in this immutable generation.
4700    ///
4701    /// # Errors
4702    ///
4703    /// Rejects invalid or absent paths and authenticated backend failures.
4704    pub async fn stat(&self, path: &str) -> Result<WorkspaceStat, WorkspaceError> {
4705        stat_generation(&self.workspace, GenerationSelector::Exact(self.id), path).await
4706    }
4707
4708    /// Resolves an ordered exact path batch while sharing authenticated directory and file-table
4709    /// frontiers. Absence is returned as `None`; links are not followed and file bodies are not
4710    /// read.
4711    ///
4712    /// # Errors
4713    ///
4714    /// Rejects an empty or excessive batch, foreign path encodings, cancellation, and
4715    /// authenticated backend failures.
4716    pub async fn lookup_paths(
4717        &self,
4718        paths: &[NamespacePath],
4719        budget: crate::WorkBudget,
4720        cancellation: &crate::CancellationToken,
4721    ) -> crate::FsResult<Vec<Option<crate::kernel::FileRecord>>> {
4722        let checkout = self
4723            .workspace
4724            .volume
4725            .checkout(
4726                GenerationSelector::Exact(self.id),
4727                CheckoutMode::read_only_pinned(),
4728                budget,
4729                cancellation,
4730            )
4731            .await?;
4732        let checkout_work = checkout.work;
4733        let remaining = checkout_work
4734            .remaining(budget)
4735            .map_err(|error| crate::OperationFailure::new(error.into(), checkout_work))?;
4736        let mut checkout = checkout.value;
4737        let lookup = checkout
4738            .lookup_batch_no_follow(paths, remaining, cancellation)
4739            .await
4740            .map_err(|failure| failure.map_with_prior_work(checkout_work, |error| error))?;
4741        let work = checkout_work
4742            .checked_add(lookup.work)
4743            .map_err(|error| crate::OperationFailure::new(error.into(), checkout_work))?;
4744        Ok(crate::FsReceipt {
4745            value: lookup
4746                .value
4747                .entries
4748                .into_iter()
4749                .map(|entry| entry.record)
4750                .collect(),
4751            work,
4752        })
4753    }
4754
4755    /// Returns one authenticated bounded directory page from this generation.
4756    ///
4757    /// # Errors
4758    ///
4759    /// Rejects invalid/absent/non-directory paths, invalid cursors or bounds,
4760    /// and authenticated backend failures.
4761    pub async fn list_directory(
4762        &self,
4763        path: &str,
4764        after: Option<&LogicalName>,
4765        maximum_entries: u32,
4766    ) -> Result<WorkspaceDirectoryPage, WorkspaceError> {
4767        list_generation_directory(
4768            &self.workspace,
4769            GenerationSelector::Exact(self.id),
4770            path,
4771            after,
4772            maximum_entries,
4773        )
4774        .await
4775    }
4776
4777    /// Finds portable paths bound to one stable file identity in this generation.
4778    ///
4779    /// The traversal is bounded by `maximum_entries`; results are sorted and
4780    /// include every matching hard link encountered before that bound.
4781    pub async fn paths_for_file_id(
4782        &self,
4783        file_id: FileId,
4784        maximum_entries: u32,
4785    ) -> Result<Vec<String>, WorkspaceError> {
4786        Ok(self
4787            .paths_for_file_ids([file_id], maximum_entries)
4788            .await?
4789            .remove(&file_id)
4790            .unwrap_or_default())
4791    }
4792
4793    pub(crate) async fn paths_for_file_id_measured(
4794        &self,
4795        file_id: FileId,
4796        maximum_entries: u32,
4797        budget: crate::WorkBudget,
4798        cancellation: &crate::CancellationToken,
4799    ) -> Result<crate::OperationReceipt<Vec<String>>, WorkspaceError> {
4800        let receipt = self
4801            .namespace_records_for_file_ids_bounded(
4802                [file_id],
4803                maximum_entries,
4804                budget,
4805                cancellation,
4806            )
4807            .await?;
4808        let paths = receipt
4809            .value
4810            .records
4811            .get(&file_id)
4812            .into_iter()
4813            .flatten()
4814            .map(|(path, _)| namespace_path_text(path))
4815            .collect::<Result<Vec<_>, _>>()?;
4816        Ok(crate::OperationReceipt {
4817            value: paths,
4818            work: receipt.work,
4819        })
4820    }
4821
4822    /// Finds portable paths for multiple stable file identities in one namespace traversal.
4823    ///
4824    /// The traversal is bounded by `maximum_entries`; each result is sorted and includes every
4825    /// matching hard link encountered before that bound. Identities without a matching binding
4826    /// are omitted.
4827    pub async fn paths_for_file_ids(
4828        &self,
4829        file_ids: impl IntoIterator<Item = FileId>,
4830        maximum_entries: u32,
4831    ) -> Result<BTreeMap<FileId, Vec<String>>, WorkspaceError> {
4832        let paths = self
4833            .namespace_paths_for_file_ids(file_ids, maximum_entries)
4834            .await?;
4835        paths
4836            .into_iter()
4837            .map(|(file_id, paths)| {
4838                paths
4839                    .into_iter()
4840                    .map(|path| namespace_path_text(&path))
4841                    .collect::<Result<Vec<_>, _>>()
4842                    .map(|paths| (file_id, paths))
4843            })
4844            .collect()
4845    }
4846
4847    async fn namespace_paths_for_file_ids(
4848        &self,
4849        file_ids: impl IntoIterator<Item = FileId>,
4850        maximum_entries: u32,
4851    ) -> Result<BTreeMap<FileId, Vec<NamespacePath>>, WorkspaceError> {
4852        Ok(self
4853            .namespace_records_for_file_ids(file_ids, maximum_entries)
4854            .await?
4855            .records
4856            .into_iter()
4857            .map(|(file_id, records)| {
4858                (file_id, records.into_iter().map(|(path, _)| path).collect())
4859            })
4860            .collect())
4861    }
4862
4863    async fn namespace_records_for_file_ids(
4864        &self,
4865        file_ids: impl IntoIterator<Item = FileId>,
4866        maximum_entries: u32,
4867    ) -> Result<NamespaceRecordSearch, WorkspaceError> {
4868        self.namespace_records_for_file_ids_bounded(
4869            file_ids,
4870            maximum_entries,
4871            crate::WorkBudget::UNBOUNDED,
4872            &crate::CancellationToken::new(),
4873        )
4874        .await
4875        .map(|receipt| receipt.value)
4876    }
4877
4878    #[allow(clippy::too_many_lines)]
4879    async fn namespace_records_for_file_ids_bounded(
4880        &self,
4881        file_ids: impl IntoIterator<Item = FileId>,
4882        maximum_entries: u32,
4883        budget: crate::WorkBudget,
4884        cancellation: &crate::CancellationToken,
4885    ) -> Result<crate::OperationReceipt<NamespaceRecordSearch>, WorkspaceError> {
4886        let file_ids: BTreeSet<_> = file_ids.into_iter().collect();
4887        if file_ids.is_empty() {
4888            return Ok(crate::OperationReceipt {
4889                value: NamespaceRecordSearch {
4890                    records: BTreeMap::new(),
4891                    complete: true,
4892                },
4893                work: crate::WorkCounters::default(),
4894            });
4895        }
4896        if maximum_entries == 0 {
4897            return Ok(crate::OperationReceipt {
4898                value: NamespaceRecordSearch {
4899                    records: BTreeMap::new(),
4900                    complete: false,
4901                },
4902                work: crate::WorkCounters::default(),
4903            });
4904        }
4905        let maximum = usize::try_from(maximum_entries).unwrap_or(usize::MAX);
4906        let limits = self.workspace.volume.config.limits;
4907        let cache_key = crate::path_index::cache_key(self.id, file_ids.iter().copied());
4908        if let Some(bytes) = self.workspace.volume.fs.path_index().load(cache_key)
4909            && let Some(cached) = crate::path_index::GenerationPathQuery::decode(
4910                &bytes,
4911                self.id,
4912                file_ids.iter().copied(),
4913            )
4914        {
4915            let cached_paths = cached
4916                .into_iter()
4917                .flat_map(|(file_id, paths)| paths.into_iter().map(move |path| (file_id, path)))
4918                .map(|(file_id, path)| path.into_namespace(limits).map(|path| (file_id, path)))
4919                .collect::<Option<Vec<_>>>();
4920            if let Some(cached_paths) = cached_paths {
4921                if cached_paths.is_empty() {
4922                    return Ok(crate::OperationReceipt {
4923                        value: NamespaceRecordSearch {
4924                            records: BTreeMap::new(),
4925                            complete: true,
4926                        },
4927                        work: crate::WorkCounters::default(),
4928                    });
4929                }
4930                // A complete cached answer larger than the caller's bound
4931                // cannot be truncated without changing which namespace paths
4932                // a cold bounded traversal would have encountered. Fall back
4933                // to that traversal instead of returning a false empty result.
4934                if cached_paths.len() <= maximum {
4935                    let paths = cached_paths
4936                        .iter()
4937                        .map(|(_, path)| path.clone())
4938                        .collect::<Vec<_>>();
4939                    let lookup = self
4940                        .lookup_paths(&paths, budget, cancellation)
4941                        .await
4942                        .map_err(WorkspaceError::engine)?;
4943                    let mut records = BTreeMap::<_, Vec<_>>::new();
4944                    let mut valid = true;
4945                    for ((expected, path), record) in cached_paths.into_iter().zip(lookup.value) {
4946                        let Some(record) = record else {
4947                            valid = false;
4948                            break;
4949                        };
4950                        if record.file_id != expected {
4951                            valid = false;
4952                            break;
4953                        }
4954                        records.entry(expected).or_default().push((path, record));
4955                    }
4956                    if valid {
4957                        return Ok(crate::OperationReceipt {
4958                            value: NamespaceRecordSearch {
4959                                records,
4960                                complete: true,
4961                            },
4962                            work: lookup.work,
4963                        });
4964                    }
4965                }
4966            }
4967        }
4968        let mut matches: BTreeMap<_, Vec<(NamespacePath, crate::kernel::FileRecord)>> =
4969            BTreeMap::new();
4970        let checkout = self
4971            .workspace
4972            .engine_checkout_measured(
4973                GenerationSelector::Exact(self.id),
4974                CheckoutMode::read_only_pinned(),
4975                budget,
4976                cancellation,
4977            )
4978            .await?;
4979        let mut work = checkout.work;
4980        let mut checkout = checkout.value;
4981        let root = NamespacePath::new(Vec::new(), limits).map_err(WorkspaceError::path)?;
4982        let root_lookup = checkout
4983            .lookup_no_follow(
4984                &root,
4985                work.remaining(budget).map_err(WorkspaceError::engine)?,
4986                cancellation,
4987            )
4988            .await
4989            .map_err(WorkspaceError::engine)?;
4990        work = work
4991            .checked_add(root_lookup.work)
4992            .map_err(WorkspaceError::engine)?;
4993        if let Some(record) = root_lookup.value.record
4994            && file_ids.contains(&record.file_id)
4995        {
4996            matches
4997                .entry(record.file_id)
4998                .or_default()
4999                .push((root.clone(), record));
5000        }
5001        let mut pending = vec![root];
5002        let mut examined = 0_usize;
5003        while let Some(directory) = pending.pop() {
5004            let mut after = None;
5005            loop {
5006                let page_receipt = checkout
5007                    .list_directory_records(
5008                        &directory,
5009                        after.as_ref(),
5010                        1_024,
5011                        work.remaining(budget).map_err(WorkspaceError::engine)?,
5012                        cancellation,
5013                    )
5014                    .await
5015                    .map_err(WorkspaceError::engine)?;
5016                work = work
5017                    .checked_add(page_receipt.work)
5018                    .map_err(WorkspaceError::engine)?;
5019                let page = page_receipt.value;
5020                for entry in &page.entries {
5021                    examined = examined.saturating_add(1);
5022                    if examined > maximum {
5023                        for records in matches.values_mut() {
5024                            records.sort_by(|left, right| left.0.cmp(&right.0));
5025                        }
5026                        return Ok(crate::OperationReceipt {
5027                            value: NamespaceRecordSearch {
5028                                records: matches,
5029                                complete: false,
5030                            },
5031                            work,
5032                        });
5033                    }
5034                    let mut components = directory.components().to_vec();
5035                    components.push(entry.name.clone());
5036                    let path =
5037                        NamespacePath::new(components, limits).map_err(WorkspaceError::path)?;
5038                    if file_ids.contains(&entry.record.file_id) {
5039                        matches
5040                            .entry(entry.record.file_id)
5041                            .or_default()
5042                            .push((path.clone(), entry.record));
5043                    }
5044                    if entry.record.kind == FileKind::Directory {
5045                        pending.push(path);
5046                    }
5047                }
5048                if !page.has_more {
5049                    break;
5050                }
5051                after = page.entries.last().map(|entry| entry.name.clone());
5052            }
5053        }
5054        for records in matches.values_mut() {
5055            records.sort_by(|left, right| left.0.cmp(&right.0));
5056        }
5057        let cached_paths = matches
5058            .iter()
5059            .map(|(&file_id, records)| {
5060                (
5061                    file_id,
5062                    records
5063                        .iter()
5064                        .map(|(path, _)| crate::path_index::CachedPath::from_namespace(path))
5065                        .collect(),
5066                )
5067            })
5068            .collect();
5069        let cached = crate::path_index::GenerationPathQuery::new(
5070            self.id,
5071            file_ids.iter().copied(),
5072            &cached_paths,
5073        );
5074        if let Some(bytes) = cached.encode() {
5075            self.workspace
5076                .volume
5077                .fs
5078                .path_index()
5079                .store(cache_key, &bytes);
5080        }
5081        Ok(crate::OperationReceipt {
5082            value: NamespaceRecordSearch {
5083                records: matches,
5084                complete: true,
5085            },
5086            work,
5087        })
5088    }
5089
5090    /// Reads one opaque symbolic-link target from this generation.
5091    ///
5092    /// # Errors
5093    ///
5094    /// Rejects invalid/absent/non-symbolic-link paths and authenticated backend
5095    /// failures.
5096    pub async fn read_symbolic_link(&self, path: &str) -> Result<Bytes, WorkspaceError> {
5097        read_generation_symbolic_link(&self.workspace, GenerationSelector::Exact(self.id), path)
5098            .await
5099    }
5100
5101    /// Plans sparse logical spans without reading content bodies.
5102    ///
5103    /// # Errors
5104    ///
5105    /// Rejects invalid/absent/non-file paths, invalid ranges or bounds, and
5106    /// authenticated backend failures.
5107    pub async fn plan_extents(
5108        &self,
5109        path: &str,
5110        offset: u64,
5111        length: u64,
5112        maximum_spans: u32,
5113    ) -> Result<WorkspaceExtentPlan, WorkspaceError> {
5114        plan_generation_extents(
5115            &self.workspace,
5116            GenerationSelector::Exact(self.id),
5117            path,
5118            offset,
5119            length,
5120            maximum_spans,
5121        )
5122        .await
5123    }
5124
5125    /// Retains this exact immutable generation under one opaque stable identity.
5126    ///
5127    /// # Errors
5128    ///
5129    /// Rejects invalid identities, foreign or corrupt generation state, and
5130    /// conflicting reuse of an existing pin identity.
5131    pub async fn pin(
5132        &self,
5133        identity: impl AsRef<str>,
5134    ) -> Result<GenerationPin<A, O>, WorkspaceError> {
5135        let identity = WorkspaceName::new(identity)?;
5136        self.workspace
5137            .volume
5138            .fs
5139            .retain_workspace_generation(
5140                &self.workspace.volume,
5141                self.id,
5142                crate::kernel::RetentionKind::Pin,
5143                identity.as_str().to_owned(),
5144            )
5145            .await?;
5146        Ok(GenerationPin {
5147            identity,
5148            generation: self.clone(),
5149        })
5150    }
5151}
5152
5153/// Exact source selection for a cheap workspace fork.
5154#[derive(Clone)]
5155pub struct ForkOptions<A, O> {
5156    pub(crate) generation: Generation<A, O>,
5157    pub(crate) idempotency_key: IdempotencyKey,
5158}
5159
5160impl<A, O> ForkOptions<A, O> {
5161    /// Forks from one exact immutable generation.
5162    #[must_use]
5163    pub fn from_generation(generation: Generation<A, O>, idempotency_key: IdempotencyKey) -> Self {
5164        Self {
5165            generation,
5166            idempotency_key,
5167        }
5168    }
5169}
5170
5171/// Canonical workspace-name validation failures.
5172#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
5173pub enum WorkspaceNameError {
5174    /// Names must contain at least one scalar.
5175    #[error("workspace name is empty")]
5176    Empty,
5177    /// Dot names are reserved for namespace traversal prevention.
5178    #[error("workspace name is reserved")]
5179    Reserved,
5180    /// Canonical UTF-8 exceeds the stable bound.
5181    #[error("workspace name exceeds 255 UTF-8 bytes")]
5182    TooLong,
5183    /// Names cannot contain separators or control characters.
5184    #[error("workspace name contains a path separator or control character")]
5185    InvalidCharacter,
5186}
5187
5188/// Customer workspace operation failure.
5189#[derive(Debug, Error)]
5190pub enum WorkspaceError {
5191    /// Friendly name was invalid.
5192    #[error(transparent)]
5193    Name(#[from] WorkspaceNameError),
5194    /// The canonical engine rejected or could not complete the operation.
5195    #[error("workspace engine failure: {0}")]
5196    Engine(String),
5197    /// Caller cancellation stopped a measured workspace operation.
5198    #[error(transparent)]
5199    Cancelled(#[from] crate::CancellationError),
5200    /// A measured workspace operation exhausted its admitted work budget.
5201    #[error(transparent)]
5202    Work(#[from] crate::WorkError),
5203    /// A fork generation belongs to another filesystem deployment.
5204    #[error("fork generation belongs to another filesystem deployment")]
5205    ForeignGeneration,
5206    /// A caller-pinned target generation is no longer current.
5207    #[error("workspace generation is stale")]
5208    StaleGeneration,
5209    /// A conditional mutation targeted a different filesystem identity.
5210    #[error("workspace file identity is stale")]
5211    StaleIdentity,
5212    /// Canonical customer path was invalid.
5213    #[error("invalid workspace path: {0}")]
5214    Path(String),
5215    /// Requested path does not exist.
5216    #[error("workspace path does not exist")]
5217    NotFound,
5218    /// An operation required a regular file.
5219    #[error("workspace path is not a regular file")]
5220    NotRegularFile,
5221    /// An operation required a directory.
5222    #[error("workspace path is not a directory")]
5223    NotDirectory,
5224    /// A bounded whole-file helper encountered a larger file.
5225    #[error("workspace file exceeds the caller's read bound")]
5226    ReadLimitExceeded,
5227    /// A retention label already identifies different immutable state.
5228    #[error("retention label is already bound to another generation")]
5229    RetentionConflict,
5230    /// Workspace semantics or lineage are incompatible for the requested join.
5231    #[error("workspaces are incompatible")]
5232    IncompatibleWorkspace,
5233    /// No common immutable ancestor exists within the admitted lineage.
5234    #[error("workspaces have no common ancestor")]
5235    NoCommonAncestor,
5236    /// Bounded lineage discovery exhausted its exact generation frontier.
5237    #[error("workspace lineage exceeds the configured bound")]
5238    LineageLimit,
5239    /// Join input, diff, or conflict bounds are zero or exhausted.
5240    #[error("workspace join exceeds its configured bound")]
5241    JoinLimit,
5242    /// A declarative merge resolution does not match its typed conflict.
5243    #[error("merge resolution is incompatible with its conflict")]
5244    InvalidMergeResolution,
5245    /// Change sets do not share one exact contiguous endpoint and deployment.
5246    #[error("change sets are not contiguous")]
5247    ChangeSetContinuity,
5248    /// Resolving changed paths exhausted the caller's namespace traversal bound.
5249    #[error("changed paths exceed the caller's traversal bound")]
5250    ChangedPathLimit,
5251    /// A staged concatenation omitted every part.
5252    #[error("staged content set is empty")]
5253    EmptyContentSet,
5254    /// The workspace has no authenticated foreign source in its ancestry.
5255    #[error("workspace is not a fork")]
5256    NotFork,
5257    /// Staged logical lengths overflowed the filesystem range.
5258    #[error("staged content length overflow")]
5259    ContentLengthOverflow,
5260}
5261
5262impl WorkspaceError {
5263    pub(crate) fn engine(error: impl fmt::Display) -> Self {
5264        Self::Engine(error.to_string())
5265    }
5266
5267    pub(crate) fn path(error: impl fmt::Display) -> Self {
5268        Self::Path(error.to_string())
5269    }
5270}
5271
5272impl From<FsError> for WorkspaceError {
5273    fn from(error: FsError) -> Self {
5274        match error {
5275            FsError::Cancelled(error) => Self::Cancelled(error),
5276            FsError::Work(error) => Self::Work(error),
5277            FsError::NotFound => Self::NotFound,
5278            FsError::NotDirectory => Self::NotDirectory,
5279            FsError::FileRead(crate::kernel::FileRangeReadError::NotRegular) => {
5280                Self::NotRegularFile
5281            }
5282            FsError::Mutation(crate::kernel::GenerationMutationError::FileIdentityConflict) => {
5283                Self::StaleIdentity
5284            }
5285            error => Self::Engine(error.to_string()),
5286        }
5287    }
5288}
5289
5290pub(crate) fn customer_path(
5291    path: &str,
5292    config: crate::model::VolumeConfig,
5293) -> Result<NamespacePath, WorkspaceError> {
5294    let portable = PortablePath::parse(path, config.limits).map_err(WorkspaceError::path)?;
5295    NamespacePath::from_portable_in_profile(&portable, config.profile, config.limits)
5296        .map_err(WorkspaceError::path)
5297}
5298
5299#[cfg(all(feature = "native-mount", not(target_arch = "wasm32")))]
5300fn merge_workspace_work<T>(
5301    prior: crate::WorkCounters,
5302    receipt: crate::OperationReceipt<T>,
5303    budget: crate::WorkBudget,
5304) -> Result<crate::OperationReceipt<T>, WorkspaceError> {
5305    let work = prior
5306        .checked_add(receipt.work)
5307        .map_err(WorkspaceError::from)?;
5308    work.verify(budget).map_err(WorkspaceError::from)?;
5309    Ok(crate::OperationReceipt {
5310        value: receipt.value,
5311        work,
5312    })
5313}
5314
5315fn path_conflict_kind(
5316    base: Option<crate::kernel::FileRecord>,
5317    current: Option<crate::kernel::FileRecord>,
5318    source: Option<crate::kernel::FileRecord>,
5319) -> ConflictKind {
5320    let (Some(base), Some(current), Some(source)) = (base, current, source) else {
5321        return ConflictKind::Binding;
5322    };
5323    if base.kind != current.kind
5324        || base.kind != source.kind
5325        || base.file_id != current.file_id
5326        || base.file_id != source.file_id
5327    {
5328        return ConflictKind::Binding;
5329    }
5330    if base.link_count != current.link_count || base.link_count != source.link_count {
5331        return ConflictKind::HardLink;
5332    }
5333    if base.metadata != current.metadata || base.metadata != source.metadata {
5334        return ConflictKind::Metadata;
5335    }
5336    match base.kind {
5337        FileKind::Regular => ConflictKind::Binary,
5338        FileKind::Directory => ConflictKind::Directory,
5339        FileKind::SymbolicLink => ConflictKind::SymbolicLink,
5340        FileKind::Fifo
5341        | FileKind::Socket
5342        | FileKind::CharacterDevice
5343        | FileKind::BlockDevice
5344        | FileKind::ReparsePoint
5345        | FileKind::MountBoundary => ConflictKind::Special,
5346    }
5347}
5348
5349fn regular_file_bytes(record: crate::kernel::FileRecord) -> Result<u64, WorkspaceError> {
5350    match record.payload {
5351        FilePayload::InlineRegular(bytes) => {
5352            Ok(u64::try_from(bytes.as_bytes().len()).unwrap_or(u64::MAX))
5353        }
5354        FilePayload::Regular { logical_bytes, .. } => Ok(logical_bytes),
5355        _ => Err(WorkspaceError::NotRegularFile),
5356    }
5357}
5358
5359#[cfg(test)]
5360#[path = "tests/workspace.rs"]
5361mod tests;