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