ralph_workflow/app/effect.rs
1//! App-level effects for pre-pipeline operations.
2//!
3//! This module defines effects that represent side effects in the CLI layer
4//! before the pipeline reducer takes over. Effects are data describing what
5//! should happen, not the execution itself.
6//!
7//! # Architecture
8//!
9//! Effects follow the functional core / imperative shell pattern:
10//! - Pure functions produce [`AppEffect`] values describing desired operations
11//! - An [`AppEffectHandler`] executes the effects, performing actual I/O
12//! - This separation enables testing without real filesystem or git operations
13//!
14//! # Example
15//!
16//! ```ignore
17//! // Pure function returns effects (testable)
18//! fn setup_workspace() -> Vec<AppEffect> {
19//! vec![
20//! AppEffect::CreateDir { path: PathBuf::from(".agent") },
21//! AppEffect::WriteFile {
22//! path: PathBuf::from(".agent/config.toml"),
23//! content: "key = value".to_string(),
24//! },
25//! ]
26//! }
27//!
28//! // Handler executes effects (I/O boundary)
29//! for effect in setup_workspace() {
30//! handler.execute(effect);
31//! }
32//! ```
33
34use serde::{Deserialize, Serialize};
35use std::path::PathBuf;
36
37/// Result of a git commit operation.
38///
39/// Indicates whether a commit was successfully created or if there were
40/// no changes to commit.
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub enum CommitResult {
43 /// Commit succeeded with the given OID (object identifier).
44 Success(String),
45 /// No changes were staged to commit.
46 NoChanges,
47}
48
49/// Result of a rebase operation.
50///
51/// Captures the various outcomes possible when rebasing a branch onto
52/// an upstream branch, including success, conflicts, and failures.
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54pub enum RebaseResult {
55 /// Rebase completed successfully with no conflicts.
56 Success,
57 /// Rebase resulted in conflicts that need resolution.
58 ///
59 /// Contains the list of conflicted file paths.
60 Conflicts(Vec<String>),
61 /// No rebase was needed (already up-to-date or same commit).
62 NoOp {
63 /// Human-readable explanation of why no rebase was needed.
64 reason: String,
65 },
66 /// Rebase failed with an error.
67 Failed(String),
68}
69
70/// App-level effects for CLI operations.
71///
72/// Each variant represents a side effect that can occur during CLI
73/// operations. Effects are data structures that describe what should
74/// happen without actually performing the operation.
75///
76/// # Categories
77///
78/// Effects are organized into logical categories:
79/// - **Working Directory**: Process working directory management
80/// - **Filesystem**: File and directory operations
81/// - **Git**: Version control operations
82/// - **Environment**: Environment variable access
83/// - **Logging**: User-facing output
84#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
85pub enum AppEffect {
86 // =========================================================================
87 // Working Directory Effects
88 // =========================================================================
89 /// Set the current working directory for the process.
90 SetCurrentDir {
91 /// The path to set as the current directory.
92 path: PathBuf,
93 },
94
95 // =========================================================================
96 // Filesystem Effects
97 // =========================================================================
98 /// Write content to a file, creating it if it doesn't exist.
99 WriteFile {
100 /// Path to the file to write.
101 path: PathBuf,
102 /// Content to write to the file.
103 content: String,
104 },
105
106 /// Read the contents of a file.
107 ReadFile {
108 /// Path to the file to read.
109 path: PathBuf,
110 },
111
112 /// Delete a file.
113 DeleteFile {
114 /// Path to the file to delete.
115 path: PathBuf,
116 },
117
118 /// Create a directory and all parent directories as needed.
119 CreateDir {
120 /// Path to the directory to create.
121 path: PathBuf,
122 },
123
124 /// Check if a path exists.
125 PathExists {
126 /// Path to check for existence.
127 path: PathBuf,
128 },
129
130 /// Set or clear the read-only flag on a file.
131 SetReadOnly {
132 /// Path to the file to modify.
133 path: PathBuf,
134 /// Whether to make the file read-only.
135 readonly: bool,
136 },
137
138 // =========================================================================
139 // Git Effects
140 // =========================================================================
141 /// Verify that we're in a git repository.
142 GitRequireRepo,
143
144 /// Get the root directory of the git repository.
145 GitGetRepoRoot,
146
147 /// Get the OID (object identifier) of HEAD.
148 GitGetHeadOid,
149
150 /// Get the diff of uncommitted changes.
151 GitDiff,
152
153 /// Get the diff from a specific commit OID to HEAD.
154 GitDiffFrom {
155 /// The starting commit OID.
156 start_oid: String,
157 },
158
159 /// Get the diff from the saved start commit to HEAD.
160 GitDiffFromStart,
161
162 /// Create a snapshot of the current state (stash-like operation).
163 GitSnapshot,
164
165 /// Stage all changes for commit.
166 GitAddAll,
167
168 /// Create a commit with the given message.
169 GitCommit {
170 /// The commit message.
171 message: String,
172 /// Optional user name for the commit author.
173 user_name: Option<String>,
174 /// Optional user email for the commit author.
175 user_email: Option<String>,
176 },
177
178 /// Save the current HEAD as the start commit reference.
179 GitSaveStartCommit,
180
181 /// Reset the start commit reference to the merge-base.
182 GitResetStartCommit,
183
184 /// Rebase the current branch onto an upstream branch.
185 GitRebaseOnto {
186 /// The upstream branch to rebase onto.
187 upstream_branch: String,
188 },
189
190 /// Get the list of files with merge conflicts.
191 GitGetConflictedFiles,
192
193 /// Continue an in-progress rebase after conflicts are resolved.
194 GitContinueRebase,
195
196 /// Abort an in-progress rebase.
197 GitAbortRebase,
198
199 /// Get the default branch name (main or master).
200 GitGetDefaultBranch,
201
202 /// Check if the current branch is main or master.
203 GitIsMainBranch,
204
205 // =========================================================================
206 // Environment Effects
207 // =========================================================================
208 /// Get the value of an environment variable.
209 GetEnvVar {
210 /// Name of the environment variable.
211 name: String,
212 },
213
214 /// Set an environment variable.
215 SetEnvVar {
216 /// Name of the environment variable.
217 name: String,
218 /// Value to set.
219 value: String,
220 },
221
222 // =========================================================================
223 // Logging Effects
224 // =========================================================================
225 /// Log an informational message.
226 LogInfo {
227 /// The message to log.
228 message: String,
229 },
230
231 /// Log a success message.
232 LogSuccess {
233 /// The message to log.
234 message: String,
235 },
236
237 /// Log a warning message.
238 LogWarn {
239 /// The message to log.
240 message: String,
241 },
242
243 /// Log an error message.
244 LogError {
245 /// The message to log.
246 message: String,
247 },
248}
249
250/// Result of executing an [`AppEffect`].
251///
252/// Each effect execution produces a result that either indicates success
253/// (with optional return data) or an error. The variant used depends on
254/// what data the effect produces.
255#[derive(Debug, Clone)]
256pub enum AppEffectResult {
257 /// Operation completed with no return value.
258 Ok,
259 /// Operation returned a string value.
260 String(String),
261 /// Operation returned a path value.
262 Path(PathBuf),
263 /// Operation returned a boolean value.
264 Bool(bool),
265 /// Commit operation result.
266 Commit(CommitResult),
267 /// Rebase operation result.
268 Rebase(RebaseResult),
269 /// Operation returned a list of strings.
270 StringList(Vec<String>),
271 /// Operation failed with an error message.
272 Error(String),
273}
274
275/// Trait for executing app-level effects.
276///
277/// Implementors of this trait perform the actual I/O operations described
278/// by [`AppEffect`] values. This separation enables:
279/// - **Testing**: Mock handlers can record effects without performing I/O
280/// - **Batching**: Handlers can optimize by batching similar operations
281/// - **Logging**: Handlers can log all operations for debugging
282///
283/// # Example
284///
285/// ```ignore
286/// struct MockHandler {
287/// effects: Vec<AppEffect>,
288/// }
289///
290/// impl AppEffectHandler for MockHandler {
291/// fn execute(&mut self, effect: AppEffect) -> AppEffectResult {
292/// self.effects.push(effect.clone());
293/// AppEffectResult::Ok
294/// }
295/// }
296/// ```
297pub trait AppEffectHandler {
298 /// Execute an effect and return the result.
299 ///
300 /// Implementations should:
301 /// - Perform the actual I/O operation described by the effect
302 /// - Return the appropriate result variant
303 /// - Return `AppEffectResult::Error` if the operation fails
304 fn execute(&mut self, effect: AppEffect) -> AppEffectResult;
305}