Skip to main content

ralph_workflow/phases/commit_logging/message_generation/
attempt_log.rs

1/// Per-attempt log for commit message generation.
2///
3/// Captures all details about a single attempt to generate a commit message,
4/// providing a complete audit trail for debugging.
5#[derive(Debug, Clone)]
6pub struct CommitAttemptLog {
7    /// Attempt number within this session
8    pub attempt_number: usize,
9    /// Agent being used (e.g., "claude", "glm")
10    pub agent: String,
11    /// Retry strategy (e.g., "initial", "`strict_json`")
12    pub strategy: String,
13    /// Timestamp when attempt started
14    pub timestamp: DateTime<Local>,
15    /// Size of the prompt in bytes
16    pub prompt_size_bytes: usize,
17    /// Size of the diff in bytes
18    pub diff_size_bytes: usize,
19    /// Whether the diff was pre-truncated
20    pub diff_was_truncated: bool,
21    /// Raw output from the agent (truncated if very large)
22    pub raw_output: Option<String>,
23    /// Extraction attempts with their results
24    pub extraction_attempts: Vec<ExtractionAttempt>,
25    /// Validation checks that were run
26    pub validation_checks: Vec<ValidationCheck>,
27    /// Final outcome of this attempt
28    pub outcome: Option<AttemptOutcome>,
29}
30
31impl CommitAttemptLog {
32    /// Create a new attempt log.
33    #[must_use] 
34    pub fn new(attempt_number: usize, agent: &str, strategy: &str) -> Self {
35        Self {
36            attempt_number,
37            agent: agent.to_string(),
38            strategy: strategy.to_string(),
39            timestamp: Local::now(),
40            prompt_size_bytes: 0,
41            diff_size_bytes: 0,
42            diff_was_truncated: false,
43            raw_output: None,
44            extraction_attempts: Vec::new(),
45            validation_checks: Vec::new(),
46            outcome: None,
47        }
48    }
49
50    /// Set the prompt size.
51    pub const fn set_prompt_size(&mut self, size: usize) {
52        self.prompt_size_bytes = size;
53    }
54
55    /// Set the diff information.
56    pub const fn set_diff_info(&mut self, size: usize, was_truncated: bool) {
57        self.diff_size_bytes = size;
58        self.diff_was_truncated = was_truncated;
59    }
60
61    /// Set the raw output from the agent.
62    ///
63    /// Truncates very large outputs to prevent log file bloat.
64    pub fn set_raw_output(&mut self, output: &str) {
65        const MAX_OUTPUT_SIZE: usize = 50_000;
66        if output.len() > MAX_OUTPUT_SIZE {
67            self.raw_output = Some(format!(
68                "{}\n\n[... truncated {} bytes ...]\n\n{}",
69                &output[..MAX_OUTPUT_SIZE / 2],
70                output.len() - MAX_OUTPUT_SIZE,
71                &output[output.len() - MAX_OUTPUT_SIZE / 2..]
72            ));
73        } else {
74            self.raw_output = Some(output.to_string());
75        }
76    }
77
78    /// Record an extraction attempt.
79    pub fn add_extraction_attempt(&mut self, attempt: ExtractionAttempt) {
80        self.extraction_attempts.push(attempt);
81    }
82
83    /// Record validation check results.
84    #[cfg(test)]
85    pub fn set_validation_checks(&mut self, checks: Vec<ValidationCheck>) {
86        self.validation_checks = checks;
87    }
88
89    /// Set the final outcome.
90    pub fn set_outcome(&mut self, outcome: AttemptOutcome) {
91        self.outcome = Some(outcome);
92    }
93
94    /// Write this log to a file using workspace abstraction.
95    ///
96    /// This is the architecture-conformant version that uses the workspace trait
97    /// instead of direct filesystem access.
98    ///
99    /// # Arguments
100    ///
101    /// * `log_dir` - Directory to write the log file to (relative to workspace)
102    /// * `workspace` - The workspace to use for filesystem operations
103    ///
104    /// # Returns
105    ///
106    /// Path to the written log file on success.
107    ///
108    /// # Errors
109    ///
110    /// Returns error if the operation fails.
111    pub fn write_to_workspace(
112        &self,
113        log_dir: &Path,
114        workspace: &dyn Workspace,
115    ) -> std::io::Result<PathBuf> {
116        // Create the log directory if needed
117        workspace.create_dir_all(log_dir)?;
118
119        // Generate filename
120        let filename = format!(
121            "attempt_{:03}_{}_{}_{}.log",
122            self.attempt_number,
123            sanitize_agent_name(&self.agent),
124            self.strategy.replace(' ', "_"),
125            self.timestamp.format("%Y%m%dT%H%M%S")
126        );
127        let log_path = log_dir.join(filename);
128
129        // Build content in memory
130        let mut content = String::new();
131        self.write_header_to_string(&mut content);
132        self.write_context_to_string(&mut content);
133        self.write_raw_output_to_string(&mut content);
134        self.write_extraction_attempts_to_string(&mut content);
135        self.write_validation_to_string(&mut content);
136        self.write_outcome_to_string(&mut content);
137
138        // Write using workspace
139        workspace.write(&log_path, &content)?;
140        Ok(log_path)
141    }
142
143    // String-based write helpers for workspace support
144    fn write_header_to_string(&self, s: &mut String) {
145        use std::fmt::Write;
146        let _ = writeln!(
147            s,
148            "========================================================================"
149        );
150        let _ = writeln!(s, "COMMIT GENERATION ATTEMPT LOG");
151        let _ = writeln!(
152            s,
153            "========================================================================"
154        );
155        let _ = writeln!(s);
156        let _ = writeln!(s, "Attempt:   #{}", self.attempt_number);
157        let _ = writeln!(s, "Agent:     {}", self.agent);
158        let _ = writeln!(s, "Strategy:  {}", self.strategy);
159        let _ = writeln!(
160            s,
161            "Timestamp: {}",
162            self.timestamp.format("%Y-%m-%d %H:%M:%S %Z")
163        );
164        let _ = writeln!(s);
165    }
166
167    fn write_context_to_string(&self, s: &mut String) {
168        use std::fmt::Write;
169        let _ = writeln!(
170            s,
171            "------------------------------------------------------------------------"
172        );
173        let _ = writeln!(s, "CONTEXT");
174        let _ = writeln!(
175            s,
176            "------------------------------------------------------------------------"
177        );
178        let _ = writeln!(s);
179        let _ = writeln!(
180            s,
181            "Prompt size: {} bytes ({} KB)",
182            self.prompt_size_bytes,
183            self.prompt_size_bytes / 1024
184        );
185        let _ = writeln!(
186            s,
187            "Diff size:   {} bytes ({} KB)",
188            self.diff_size_bytes,
189            self.diff_size_bytes / 1024
190        );
191        let _ = writeln!(
192            s,
193            "Diff truncated: {}",
194            if self.diff_was_truncated { "YES" } else { "NO" }
195        );
196        let _ = writeln!(s);
197    }
198
199    fn write_raw_output_to_string(&self, s: &mut String) {
200        use std::fmt::Write;
201        let _ = writeln!(
202            s,
203            "------------------------------------------------------------------------"
204        );
205        let _ = writeln!(s, "RAW AGENT OUTPUT");
206        let _ = writeln!(
207            s,
208            "------------------------------------------------------------------------"
209        );
210        let _ = writeln!(s);
211        match &self.raw_output {
212            Some(output) => {
213                let _ = writeln!(s, "{output}");
214            }
215            None => {
216                let _ = writeln!(s, "[No output captured]");
217            }
218        }
219        let _ = writeln!(s);
220    }
221
222    fn write_extraction_attempts_to_string(&self, s: &mut String) {
223        use std::fmt::Write;
224        let _ = writeln!(
225            s,
226            "------------------------------------------------------------------------"
227        );
228        let _ = writeln!(s, "EXTRACTION ATTEMPTS");
229        let _ = writeln!(
230            s,
231            "------------------------------------------------------------------------"
232        );
233        let _ = writeln!(s);
234
235        if self.extraction_attempts.is_empty() {
236            let _ = writeln!(s, "[No extraction attempts recorded]");
237        } else {
238            for (i, attempt) in self.extraction_attempts.iter().enumerate() {
239                let status = if attempt.success {
240                    "✓ SUCCESS"
241                } else {
242                    "✗ FAILED"
243                };
244                let _ = writeln!(s, "{}. {} [{}]", i + 1, attempt.method, status);
245                let _ = writeln!(s, "   Detail: {}", attempt.detail);
246                let _ = writeln!(s);
247            }
248        }
249        let _ = writeln!(s);
250    }
251
252    fn write_validation_to_string(&self, s: &mut String) {
253        use std::fmt::Write;
254        let _ = writeln!(
255            s,
256            "------------------------------------------------------------------------"
257        );
258        let _ = writeln!(s, "VALIDATION RESULTS");
259        let _ = writeln!(
260            s,
261            "------------------------------------------------------------------------"
262        );
263        let _ = writeln!(s);
264
265        if self.validation_checks.is_empty() {
266            let _ = writeln!(s, "[No validation checks recorded]");
267        } else {
268            for check in &self.validation_checks {
269                let status = if check.passed { "✓ PASS" } else { "✗ FAIL" };
270                let _ = write!(s, "  [{status}] {}", check.name);
271                if let Some(error) = &check.error {
272                    let _ = writeln!(s, ": {error}");
273                } else {
274                    let _ = writeln!(s);
275                }
276            }
277        }
278        let _ = writeln!(s);
279    }
280
281    fn write_outcome_to_string(&self, s: &mut String) {
282        use std::fmt::Write;
283        let _ = writeln!(
284            s,
285            "------------------------------------------------------------------------"
286        );
287        let _ = writeln!(s, "OUTCOME");
288        let _ = writeln!(
289            s,
290            "------------------------------------------------------------------------"
291        );
292        let _ = writeln!(s);
293        match &self.outcome {
294            Some(outcome) => {
295                let _ = writeln!(s, "{outcome}");
296            }
297            None => {
298                let _ = writeln!(s, "[Outcome not recorded]");
299            }
300        }
301        let _ = writeln!(s);
302        let _ = writeln!(
303            s,
304            "========================================================================"
305        );
306    }
307}
308
309/// Sanitize agent name for use in filename.
310fn sanitize_agent_name(agent: &str) -> String {
311    agent
312        .chars()
313        .map(|c| if c.is_alphanumeric() { c } else { '_' })
314        .collect::<String>()
315        .chars()
316        .take(MAX_AGENT_NAME_LENGTH)
317        .collect()
318}