1use crate::errors::{CoreError, CoreResult};
10use serde::{Deserialize, Serialize};
11use std::path::{Path, PathBuf};
12
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14#[serde(rename_all = "camelCase")]
15pub struct RalphHistoryEntry {
17 pub timestamp: i64,
19 pub duration: i64,
21 pub completion_promise_found: bool,
23 pub file_changes_count: u32,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
28#[serde(rename_all = "camelCase")]
29pub struct RalphState {
31 pub change_id: String,
33 pub iteration: u32,
35 pub history: Vec<RalphHistoryEntry>,
37 pub context_file: String,
39}
40
41pub fn ralph_state_dir(ito_path: &Path, change_id: &str) -> PathBuf {
43 ito_path.join(".state").join("ralph").join(change_id)
44}
45
46pub fn ralph_state_json_path(ito_path: &Path, change_id: &str) -> PathBuf {
48 ralph_state_dir(ito_path, change_id).join("state.json")
49}
50
51pub fn ralph_context_path(ito_path: &Path, change_id: &str) -> PathBuf {
53 ralph_state_dir(ito_path, change_id).join("context.md")
54}
55
56pub fn load_state(ito_path: &Path, change_id: &str) -> CoreResult<Option<RalphState>> {
58 let p = ralph_state_json_path(ito_path, change_id);
59 if !p.exists() {
60 return Ok(None);
61 }
62 let raw = ito_common::io::read_to_string_std(&p)
63 .map_err(|e| CoreError::io(format!("reading {}", p.display()), e))?;
64 let state = serde_json::from_str(&raw)
65 .map_err(|e| CoreError::Parse(format!("JSON error parsing {p}: {e}", p = p.display())))?;
66 Ok(Some(state))
67}
68
69pub fn save_state(ito_path: &Path, change_id: &str, state: &RalphState) -> CoreResult<()> {
71 let dir = ralph_state_dir(ito_path, change_id);
72 ito_common::io::create_dir_all_std(&dir)
73 .map_err(|e| CoreError::io(format!("creating directory {}", dir.display()), e))?;
74 let p = ralph_state_json_path(ito_path, change_id);
75 let raw = serde_json::to_string_pretty(state)
76 .map_err(|e| CoreError::Parse(format!("JSON error serializing state: {e}")))?;
77 ito_common::io::write_std(&p, raw)
78 .map_err(|e| CoreError::io(format!("writing {}", p.display()), e))?;
79 Ok(())
80}
81
82pub fn load_context(ito_path: &Path, change_id: &str) -> CoreResult<String> {
86 let p = ralph_context_path(ito_path, change_id);
87 if !p.exists() {
88 return Ok(String::new());
89 }
90 ito_common::io::read_to_string_std(&p)
91 .map_err(|e| CoreError::io(format!("reading {}", p.display()), e))
92}
93
94pub fn append_context(ito_path: &Path, change_id: &str, text: &str) -> CoreResult<()> {
98 let dir = ralph_state_dir(ito_path, change_id);
99 ito_common::io::create_dir_all_std(&dir)
100 .map_err(|e| CoreError::io(format!("creating directory {}", dir.display()), e))?;
101 let p = ralph_context_path(ito_path, change_id);
102 let existing_result = ito_common::io::read_to_string_std(&p);
103 let mut existing = match existing_result {
104 Ok(s) => s,
105 Err(e) if e.kind() == std::io::ErrorKind::NotFound => String::new(),
106 Err(e) => return Err(CoreError::io(format!("reading {}", p.display()), e)),
107 };
108
109 let trimmed = text.trim();
110 if trimmed.is_empty() {
111 return Ok(());
112 }
113
114 if !existing.trim().is_empty() {
115 existing.push_str("\n\n");
116 }
117 existing.push_str(trimmed);
118 existing.push('\n');
119 ito_common::io::write_std(&p, existing)
120 .map_err(|e| CoreError::io(format!("writing {}", p.display()), e))?;
121 Ok(())
122}
123
124pub fn clear_context(ito_path: &Path, change_id: &str) -> CoreResult<()> {
126 let dir = ralph_state_dir(ito_path, change_id);
127 ito_common::io::create_dir_all_std(&dir)
128 .map_err(|e| CoreError::io(format!("creating directory {}", dir.display()), e))?;
129 let p = ralph_context_path(ito_path, change_id);
130 ito_common::io::write_std(&p, "")
131 .map_err(|e| CoreError::io(format!("writing {}", p.display()), e))?;
132 Ok(())
133}