ralph_workflow/app/resume/
validation.rs1#[derive(Debug, Clone, PartialEq, Eq)]
6pub enum ValidationOutcome {
7 Passed,
9 Failed(String),
11}
12
13pub(crate) fn validate_file_system_state(
22 file_system_state: &FileSystemState,
23 logger: &Logger,
24 strategy: crate::checkpoint::recovery::RecoveryStrategy,
25 workspace: &dyn Workspace,
26) -> ValidationOutcome {
27 let errors = file_system_state.validate_with_workspace(workspace, None);
28
29 if errors.is_empty() {
30 logger.info("File system state validation passed.");
31 return ValidationOutcome::Passed;
32 }
33
34 logger.warn("File system state validation detected changes:");
35
36 for error in &errors {
37 let (problem, commands) = error.recovery_commands();
38 logger.warn(&format!(" - {}", error));
39 logger.info(&format!(" What's wrong: {}", problem));
40 logger.info(" How to fix:");
41 for cmd in commands {
42 logger.info(&format!(" {}", cmd));
43 }
44 }
45
46 match strategy {
48 crate::checkpoint::recovery::RecoveryStrategy::Fail => {
49 logger.error("File system state validation failed (strategy: fail).");
50 logger.info("Use --recovery-strategy=auto to attempt automatic recovery.");
51 logger.info("Use --recovery-strategy=force to proceed anyway (not recommended).");
52 ValidationOutcome::Failed(
53 "File system state changed - see errors above or use --recovery-strategy=force to proceed anyway".to_string()
54 )
55 }
56 crate::checkpoint::recovery::RecoveryStrategy::Force => {
57 logger.warn("Proceeding with resume despite file changes (strategy: force).");
58 logger.info("Note: Pipeline behavior may be unpredictable.");
59 ValidationOutcome::Passed
60 }
61 crate::checkpoint::recovery::RecoveryStrategy::Auto => {
62 let (_recovered, remaining) =
64 attempt_auto_recovery(file_system_state, &errors, logger, workspace);
65
66 if remaining.is_empty() {
67 logger.success("Automatic recovery completed successfully.");
68 ValidationOutcome::Passed
69 } else {
70 logger.warn("Some issues could not be automatically recovered:");
71 for error in &remaining {
72 logger.warn(&format!(" - {}", error));
73 }
74 logger.warn("Proceeding with resume despite unrecovered issues (strategy: auto).");
75 logger.info("Note: Pipeline behavior may be unpredictable.");
76 ValidationOutcome::Passed
77 }
78 }
79 }
80}
81
82fn attempt_auto_recovery(
98 file_system_state: &FileSystemState,
99 errors: &[ValidationError],
100 logger: &Logger,
101 workspace: &dyn Workspace,
102) -> (usize, Vec<ValidationError>) {
103 let mut recovered = 0;
104 let mut remaining = Vec::new();
105
106 for error in errors {
107 match attempt_recovery_for_error(file_system_state, error, logger, workspace) {
108 Ok(()) => {
109 recovered += 1;
110 logger.success(&format!("Recovered: {}", error));
111 }
112 Err(e) => {
113 remaining.push(error.clone());
114 logger.warn(&format!("Could not recover: {} - {}", error, e));
115 }
116 }
117 }
118
119 (recovered, remaining)
120}
121
122fn attempt_recovery_for_error(
128 file_system_state: &FileSystemState,
129 error: &ValidationError,
130 logger: &Logger,
131 workspace: &dyn Workspace,
132) -> Result<(), String> {
133 match error {
134 ValidationError::FileContentChanged { path } => {
135 if let Some(snapshot) = file_system_state.files.get(path) {
137 if let Some(content) = snapshot.get_content() {
138 workspace
139 .write(Path::new(path), &content)
140 .map_err(|e| format!("Failed to write file: {}", e))?;
141 logger.info(&format!("Restored {} from checkpoint content.", path));
142 return Ok(());
143 }
144 }
145 Err("No content available in snapshot".to_string())
146 }
147 ValidationError::GitHeadChanged { .. } => {
148 Err("Git HEAD changes require manual intervention".to_string())
151 }
152 ValidationError::GitStateInvalid { .. } => {
153 Err("Git state validation requires manual intervention".to_string())
154 }
155 ValidationError::GitWorkingTreeChanged { .. } => {
156 Err("Git working tree changes require manual intervention".to_string())
158 }
159 ValidationError::FileMissing { path } => {
160 if let Some(snapshot) = file_system_state.files.get(path) {
162 if let Some(content) = snapshot.get_content() {
163 workspace
164 .write(Path::new(path), &content)
165 .map_err(|e| format!("Failed to write file: {}", e))?;
166 logger.info(&format!("Restored missing {} from checkpoint.", path));
167 return Ok(());
168 }
169 }
170 Err(format!("Cannot recover missing file {}", path))
171 }
172 ValidationError::FileUnexpectedlyExists { path } => {
173 Err(format!(
175 "File {} should not exist - requires manual removal",
176 path
177 ))
178 }
179 }
180}
181
182pub(crate) fn check_rebase_state_on_resume(checkpoint: &PipelineCheckpoint, logger: &Logger) {
187 let is_rebase_phase = matches!(
189 checkpoint.phase,
190 PipelinePhase::PreRebase
191 | PipelinePhase::PreRebaseConflict
192 | PipelinePhase::PostRebase
193 | PipelinePhase::PostRebaseConflict
194 );
195
196 if !is_rebase_phase {
197 return;
198 }
199
200 match rebase_in_progress() {
201 Ok(true) => {
202 logger.warn("A git rebase is currently in progress.");
203 logger.info("The checkpoint indicates you were in a rebase phase.");
204 logger.info("Options:");
205 logger.info(" - Continue: Let ralph complete the rebase process");
206 logger.info(" - Abort manually: Run 'git rebase --abort' then use --resume");
207 }
208 Ok(false) => {
209 logger.info("No git rebase is currently in progress.");
212 }
213 Err(e) => {
214 logger.warn(&format!("Could not check rebase state: {e}"));
215 }
216 }
217}