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 } else {
69 logger.warn("Some issues could not be automatically recovered:");
70 for error in &remaining {
71 logger.warn(&format!(" - {error}"));
72 }
73 logger.warn("Proceeding with resume despite unrecovered issues (strategy: auto).");
74 logger.info("Note: Pipeline behavior may be unpredictable.");
75 }
76 ValidationOutcome::Passed
77 }
78 }
79}
80
81fn attempt_auto_recovery(
97 file_system_state: &FileSystemState,
98 errors: &[ValidationError],
99 logger: &Logger,
100 workspace: &dyn Workspace,
101) -> (usize, Vec<ValidationError>) {
102 let mut recovered = 0;
103 let mut remaining = Vec::new();
104
105 for error in errors {
106 match attempt_recovery_for_error(file_system_state, error, logger, workspace) {
107 Ok(()) => {
108 recovered += 1;
109 logger.success(&format!("Recovered: {error}"));
110 }
111 Err(e) => {
112 remaining.push(error.clone());
113 logger.warn(&format!("Could not recover: {error} - {e}"));
114 }
115 }
116 }
117
118 (recovered, remaining)
119}
120
121fn attempt_recovery_for_error(
127 file_system_state: &FileSystemState,
128 error: &ValidationError,
129 logger: &Logger,
130 workspace: &dyn Workspace,
131) -> Result<(), String> {
132 match error {
133 ValidationError::FileContentChanged { path } => {
134 if let Some(snapshot) = file_system_state.files.get(path) {
136 if let Some(content) = snapshot.get_content() {
137 workspace
138 .write(Path::new(path), &content)
139 .map_err(|e| format!("Failed to write file: {e}"))?;
140 logger.info(&format!("Restored {path} from checkpoint content."));
141 return Ok(());
142 }
143 }
144 Err("No content available in snapshot".to_string())
145 }
146 ValidationError::GitHeadChanged { .. } => {
147 Err("Git HEAD changes require manual intervention".to_string())
150 }
151 ValidationError::GitStateInvalid { .. } => {
152 Err("Git state validation requires manual intervention".to_string())
153 }
154 ValidationError::GitWorkingTreeChanged { .. } => {
155 Err("Git working tree changes require manual intervention".to_string())
157 }
158 ValidationError::FileMissing { path } => {
159 if let Some(snapshot) = file_system_state.files.get(path) {
161 if let Some(content) = snapshot.get_content() {
162 workspace
163 .write(Path::new(path), &content)
164 .map_err(|e| format!("Failed to write file: {e}"))?;
165 logger.info(&format!("Restored missing {path} from checkpoint."));
166 return Ok(());
167 }
168 }
169 Err(format!("Cannot recover missing file {path}"))
170 }
171 ValidationError::FileUnexpectedlyExists { path } => {
172 Err(format!(
174 "File {path} should not exist - requires manual removal"
175 ))
176 }
177 }
178}
179
180pub(crate) fn check_rebase_state_on_resume(checkpoint: &PipelineCheckpoint, logger: &Logger) {
185 let is_rebase_phase = matches!(
187 checkpoint.phase,
188 PipelinePhase::PreRebase
189 | PipelinePhase::PreRebaseConflict
190 | PipelinePhase::PostRebase
191 | PipelinePhase::PostRebaseConflict
192 );
193
194 if !is_rebase_phase {
195 return;
196 }
197
198 match rebase_in_progress() {
199 Ok(true) => {
200 logger.warn("A git rebase is currently in progress.");
201 logger.info("The checkpoint indicates you were in a rebase phase.");
202 logger.info("Options:");
203 logger.info(" - Continue: Let ralph complete the rebase process");
204 logger.info(" - Abort manually: Run 'git rebase --abort' then use --resume");
205 }
206 Ok(false) => {
207 logger.info("No git rebase is currently in progress.");
210 }
211 Err(e) => {
212 logger.warn(&format!("Could not check rebase state: {e}"));
213 }
214 }
215}