codetether_agent/ralph/
types.rs1use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct UserStory {
9 pub id: String,
11
12 pub title: String,
14
15 pub description: String,
17
18 #[serde(default)]
20 pub acceptance_criteria: Vec<String>,
21
22 #[serde(default)]
24 pub passes: bool,
25
26 #[serde(default = "default_priority")]
28 pub priority: u8,
29
30 #[serde(default)]
32 pub depends_on: Vec<String>,
33
34 #[serde(default = "default_complexity")]
36 pub complexity: u8,
37}
38
39fn default_priority() -> u8 { 1 }
40fn default_complexity() -> u8 { 3 }
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct Prd {
45 pub project: String,
47
48 pub feature: String,
50
51 #[serde(default)]
53 pub branch_name: String,
54
55 #[serde(default = "default_version")]
57 pub version: String,
58
59 #[serde(default)]
61 pub user_stories: Vec<UserStory>,
62
63 #[serde(default)]
65 pub technical_requirements: Vec<String>,
66
67 #[serde(default)]
69 pub quality_checks: QualityChecks,
70
71 #[serde(default)]
73 pub created_at: String,
74
75 #[serde(default)]
77 pub updated_at: String,
78}
79
80fn default_version() -> String { "1.0".to_string() }
81
82#[derive(Debug, Clone, Default, Serialize, Deserialize)]
84pub struct QualityChecks {
85 #[serde(default)]
87 pub typecheck: Option<String>,
88
89 #[serde(default)]
91 pub test: Option<String>,
92
93 #[serde(default)]
95 pub lint: Option<String>,
96
97 #[serde(default)]
99 pub build: Option<String>,
100}
101
102impl Prd {
103 pub async fn load(path: &PathBuf) -> anyhow::Result<Self> {
105 let content = tokio::fs::read_to_string(path).await?;
106 let prd: Prd = serde_json::from_str(&content)?;
107 Ok(prd)
108 }
109
110 pub async fn save(&self, path: &PathBuf) -> anyhow::Result<()> {
112 let content = serde_json::to_string_pretty(self)?;
113 tokio::fs::write(path, content).await?;
114 Ok(())
115 }
116
117 pub fn next_story(&self) -> Option<&UserStory> {
119 self.user_stories
120 .iter()
121 .filter(|s| !s.passes)
122 .filter(|s| self.dependencies_met(&s.depends_on))
123 .min_by_key(|s| (s.priority, s.complexity))
124 }
125
126 fn dependencies_met(&self, deps: &[String]) -> bool {
128 deps.iter().all(|dep_id| {
129 self.user_stories
130 .iter()
131 .find(|s| s.id == *dep_id)
132 .map(|s| s.passes)
133 .unwrap_or(true)
134 })
135 }
136
137 pub fn passed_count(&self) -> usize {
139 self.user_stories.iter().filter(|s| s.passes).count()
140 }
141
142 pub fn is_complete(&self) -> bool {
144 self.user_stories.iter().all(|s| s.passes)
145 }
146
147 pub fn mark_passed(&mut self, story_id: &str) {
149 if let Some(story) = self.user_stories.iter_mut().find(|s| s.id == *story_id) {
150 story.passes = true;
151 }
152 }
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct RalphState {
158 pub prd: Prd,
160
161 pub current_iteration: usize,
163
164 pub max_iterations: usize,
166
167 pub status: RalphStatus,
169
170 #[serde(default)]
172 pub progress_log: Vec<ProgressEntry>,
173
174 pub prd_path: PathBuf,
176
177 pub working_dir: PathBuf,
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
183#[serde(rename_all = "snake_case")]
184pub enum RalphStatus {
185 Pending,
186 Running,
187 Completed,
188 MaxIterations,
189 Stopped,
190 QualityFailed,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct ProgressEntry {
196 pub story_id: String,
198
199 pub iteration: usize,
201
202 pub status: String,
204
205 #[serde(default)]
207 pub learnings: Vec<String>,
208
209 #[serde(default)]
211 pub files_changed: Vec<String>,
212
213 pub timestamp: String,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct RalphConfig {
220 #[serde(default = "default_prd_path")]
222 pub prd_path: String,
223
224 #[serde(default = "default_max_iterations")]
226 pub max_iterations: usize,
227
228 #[serde(default = "default_progress_path")]
230 pub progress_path: String,
231
232 #[serde(default = "default_auto_commit")]
234 pub auto_commit: bool,
235
236 #[serde(default = "default_quality_checks_enabled")]
238 pub quality_checks_enabled: bool,
239
240 #[serde(default)]
242 pub model: Option<String>,
243
244 #[serde(default)]
246 pub use_rlm: bool,
247}
248
249fn default_prd_path() -> String { "prd.json".to_string() }
250fn default_max_iterations() -> usize { 10 }
251fn default_progress_path() -> String { "progress.txt".to_string() }
252fn default_auto_commit() -> bool { false }
253fn default_quality_checks_enabled() -> bool { true }
254
255impl Default for RalphConfig {
256 fn default() -> Self {
257 Self {
258 prd_path: default_prd_path(),
259 max_iterations: default_max_iterations(),
260 progress_path: default_progress_path(),
261 auto_commit: default_auto_commit(),
262 quality_checks_enabled: default_quality_checks_enabled(),
263 model: None,
264 use_rlm: false,
265 }
266 }
267}