Skip to main content

claw_branch/
error.rs

1//! Error types for branch orchestration.
2
3use std::path::PathBuf;
4
5use thiserror::Error;
6use uuid::Uuid;
7
8/// The result type used across the branch engine.
9pub type BranchResult<T> = Result<T, BranchError>;
10
11/// Enumerates errors produced by branching, snapshotting, merging, and metrics code.
12#[derive(Debug, Error)]
13pub enum BranchError {
14    /// Indicates a runtime configuration is invalid.
15    #[error("invalid config: {0}")]
16    InvalidConfig(String),
17    /// Indicates permission checks denied the operation.
18    #[error("permission denied: {0}")]
19    PermissionDenied(String),
20    /// Indicates branch data escaped workspace-scoped query isolation.
21    #[error("workspace isolation violation: expected {expected}, found {found}")]
22    WorkspaceIsolationViolation {
23        /// The expected workspace id configured for this engine.
24        expected: Uuid,
25        /// The unexpected workspace id returned by persistence.
26        found: Uuid,
27    },
28    /// Indicates a branch name failed strict path-safe validation.
29    #[error("invalid branch name: {0}")]
30    InvalidBranchName(String),
31    /// Indicates the workspace has reached its configured branch limit.
32    #[error("branch limit exceeded")]
33    BranchLimitExceeded,
34    /// Wraps SQLx database failures.
35    #[error("database error: {0}")]
36    Database(#[from] sqlx::Error),
37    /// Wraps SQLx migration failures.
38    #[error("migration error: {0}")]
39    Migration(#[from] sqlx::migrate::MigrateError),
40    /// Wraps filesystem errors.
41    #[error("io error: {0}")]
42    Io(#[from] std::io::Error),
43    /// Wraps JSON serialization errors.
44    #[error("serialization error: {0}")]
45    Serialization(#[from] serde_json::Error),
46    /// Indicates a branch lookup failed.
47    #[error("branch not found: {0}")]
48    BranchNotFound(Uuid),
49    /// Indicates a workspace already has a branch with the same logical name.
50    #[error("branch already exists: {0}")]
51    BranchAlreadyExists(String),
52    /// Indicates an operation requires an active branch.
53    #[error("branch {id} is not active: {status}")]
54    BranchNotActive {
55        /// The branch identifier that failed the active-state check.
56        id: Uuid,
57        /// The current lifecycle status string for the branch.
58        status: String,
59    },
60    /// Indicates snapshot creation or restore failed.
61    #[error("snapshot failed for branch {branch_id}: {reason}")]
62    SnapshotFailed {
63        /// The branch identifier associated with the snapshot failure.
64        branch_id: Uuid,
65        /// The human-readable failure reason.
66        reason: String,
67    },
68    /// Indicates a snapshot file failed integrity validation.
69    #[error("snapshot corrupt for branch {branch_id} at {path:?}")]
70    SnapshotCorrupt {
71        /// The branch identifier associated with the corrupt snapshot.
72        branch_id: Uuid,
73        /// The path to the corrupt snapshot file.
74        path: PathBuf,
75    },
76    /// Indicates a required snapshot hash sidecar file is missing.
77    #[error("snapshot hash missing for branch {branch_id} at {path:?}")]
78    SnapshotHashMissing {
79        /// The branch identifier associated with the missing hash sidecar.
80        branch_id: Uuid,
81        /// The path to the expected sidecar hash file.
82        path: PathBuf,
83    },
84    /// Indicates merge conflicts were not fully resolved.
85    #[error("merge conflicts unresolved for entities: {entity_ids:?}")]
86    MergeConflictUnresolved {
87        /// The entity identifiers that still have unresolved conflicts.
88        entity_ids: Vec<String>,
89    },
90    /// Indicates two branches cannot be merged with the selected base.
91    #[error("merge incompatible base for ours={ours} theirs={theirs}: {reason}")]
92    MergeIncompatibleBase {
93        /// The local branch identifier.
94        ours: Uuid,
95        /// The incoming branch identifier.
96        theirs: Uuid,
97        /// The reason the base is incompatible.
98        reason: String,
99    },
100    /// Indicates a lineage edge would create a DAG cycle.
101    #[error("dag cycle detected from {from} to {to}")]
102    DagCycle {
103        /// The proposed source branch id.
104        from: Uuid,
105        /// The proposed destination branch id.
106        to: Uuid,
107    },
108    /// Indicates a branch operation would introduce a lineage cycle.
109    #[error("cycle detected from {from} to {to}")]
110    CycleDetected {
111        /// The proposed source branch id.
112        from: Uuid,
113        /// The proposed destination branch id.
114        to: Uuid,
115    },
116    /// Indicates a DAG node lookup failed.
117    #[error("dag node not found: {0}")]
118    DagNodeNotFound(Uuid),
119    /// Indicates diff extraction failed.
120    #[error("diff failed between {branch_a} and {branch_b}: {reason}")]
121    DiffFailed {
122        /// The first branch identifier in the failed diff.
123        branch_a: Uuid,
124        /// The second branch identifier in the failed diff.
125        branch_b: Uuid,
126        /// The human-readable diff failure reason.
127        reason: String,
128    },
129    /// Indicates a selective commit failed validation.
130    #[error("commit validation failed for branch {branch_id}: {violations:?}")]
131    CommitValidationFailed {
132        /// The branch identifier whose commit was rejected.
133        branch_id: Uuid,
134        /// The list of validation violations.
135        violations: Vec<String>,
136    },
137    /// Indicates sandbox execution failed.
138    #[error("sandbox error: {0}")]
139    SandboxError(String),
140    /// Indicates metrics handling failed.
141    #[error("metrics error: {0}")]
142    MetricsError(String),
143    /// Indicates a branch name failed validation.
144    #[error("naming error: {0}")]
145    NamingError(String),
146    /// Indicates an orphaned snapshot path was encountered.
147    #[error("orphaned snapshot: {0:?}")]
148    OrphanedSnapshot(PathBuf),
149}
150
151impl BranchError {
152    /// Returns true if this error indicates a missing branch or DAG node.
153    pub fn is_not_found(&self) -> bool {
154        matches!(self, Self::BranchNotFound(_) | Self::DagNodeNotFound(_))
155    }
156
157    /// Returns true if this error represents an unresolved merge conflict.
158    pub fn is_merge_conflict(&self) -> bool {
159        matches!(
160            self,
161            Self::MergeConflictUnresolved { .. } | Self::MergeIncompatibleBase { .. }
162        )
163    }
164
165    /// Returns true if this error belongs to the snapshot subsystem.
166    pub fn is_snapshot_error(&self) -> bool {
167        matches!(
168            self,
169            Self::SnapshotFailed { .. }
170                | Self::SnapshotCorrupt { .. }
171                | Self::SnapshotHashMissing { .. }
172                | Self::OrphanedSnapshot(_)
173        )
174    }
175
176    /// Returns true if this error belongs to DAG management.
177    pub fn is_dag_error(&self) -> bool {
178        matches!(
179            self,
180            Self::DagCycle { .. } | Self::CycleDetected { .. } | Self::DagNodeNotFound(_)
181        )
182    }
183}