Skip to main content

ralph_workflow/phases/commit_logging/
io.rs

1// phases/commit_logging/io.rs — boundary module for mutable session state.
2// File stem is `io` — recognized as boundary module by functional lints.
3// Mutable-receiver methods on CommitLogSession live here to comply with
4// forbid_mutating_receiver_methods in non-boundary code.
5
6/// Session tracker for commit generation logging.
7///
8/// Manages a unique run directory for a commit generation session,
9/// ensuring log files are organized and don't overwrite each other.
10#[derive(Debug)]
11pub struct CommitLogSession {
12    /// Base log directory
13    run_dir: PathBuf,
14    /// Current attempt counter
15    attempt_counter: usize,
16}
17
18impl CommitLogSession {
19    /// Create a new logging session using workspace abstraction.
20    ///
21    /// Creates a unique run directory under the base log path.
22    ///
23    /// # Errors
24    ///
25    /// Returns error if the operation fails.
26    pub fn new(base_log_dir: &str, workspace: &dyn Workspace) -> std::io::Result<Self> {
27        let timestamp = Local::now().format("%Y%m%d_%H%M%S");
28        let run_dir = PathBuf::from(base_log_dir).join(format!("run_{timestamp}"));
29        workspace.create_dir_all(&run_dir)?;
30
31        Ok(Self {
32            run_dir,
33            attempt_counter: 0,
34        })
35    }
36
37    /// Create a no-op logging session that discards all writes.
38    #[must_use]
39    pub fn noop() -> Self {
40        Self {
41            run_dir: PathBuf::from("/dev/null/ralph-noop-session"),
42            attempt_counter: 0,
43        }
44    }
45
46    /// Check if this is a no-op session.
47    #[must_use]
48    pub fn is_noop(&self) -> bool {
49        self.run_dir.starts_with("/dev/null")
50    }
51
52    /// Get the path to the run directory.
53    #[must_use]
54    pub fn run_dir(&self) -> &Path {
55        &self.run_dir
56    }
57
58    /// Get the next attempt number and increment the counter.
59    pub fn next_attempt_number(&mut self) -> usize {
60        self.attempt_counter = self.attempt_counter.saturating_add(1);
61        self.attempt_counter
62    }
63
64    /// Create a new attempt log for this session.
65    pub fn new_attempt(&mut self, agent: &str, strategy: &str) -> CommitAttemptLog {
66        let attempt_number = self.next_attempt_number();
67        CommitAttemptLog::new(attempt_number, agent, strategy)
68    }
69
70    /// Write summary file at end of session.
71    ///
72    /// For noop sessions, this silently succeeds without writing anything.
73    ///
74    /// # Errors
75    ///
76    /// Returns error if the operation fails.
77    pub fn write_summary(
78        &self,
79        total_attempts: usize,
80        final_outcome: &str,
81        workspace: &dyn Workspace,
82    ) -> std::io::Result<()> {
83        if self.is_noop() {
84            return Ok(());
85        }
86
87        let summary_path = self.run_dir.join("SUMMARY.txt");
88
89        let content = format!(
90            "COMMIT GENERATION SESSION SUMMARY\n\
91             =================================\n\
92             \n\
93             Run directory: {}\n\
94             Total attempts: {}\n\
95             Final outcome: {}\n\
96             \n\
97             Individual attempt logs are in this directory.\n",
98            self.run_dir.display(),
99            total_attempts,
100            final_outcome
101        );
102
103        workspace.write(&summary_path, &content)?;
104        Ok(())
105    }
106}