1use 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, alias = "dependencies")]
32 pub depends_on: Vec<String>,
33
34 #[serde(default = "default_complexity")]
36 pub complexity: u8,
37}
38
39fn default_priority() -> u8 {
40 1
41}
42fn default_complexity() -> u8 {
43 3
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct Prd {
49 pub project: String,
51
52 pub feature: String,
54
55 #[serde(default)]
57 pub branch_name: String,
58
59 #[serde(default = "default_version")]
61 pub version: String,
62
63 #[serde(default)]
65 pub user_stories: Vec<UserStory>,
66
67 #[serde(default)]
69 pub technical_requirements: Vec<String>,
70
71 #[serde(default)]
73 pub quality_checks: QualityChecks,
74
75 #[serde(default)]
77 pub created_at: String,
78
79 #[serde(default)]
81 pub updated_at: String,
82}
83
84fn default_version() -> String {
85 "1.0".to_string()
86}
87
88#[derive(Debug, Clone, Default, Serialize, Deserialize)]
90pub struct QualityChecks {
91 #[serde(default)]
93 pub typecheck: Option<String>,
94
95 #[serde(default)]
97 pub test: Option<String>,
98
99 #[serde(default)]
101 pub lint: Option<String>,
102
103 #[serde(default)]
105 pub build: Option<String>,
106}
107
108impl Prd {
109 pub async fn load(path: &PathBuf) -> anyhow::Result<Self> {
111 let content = tokio::fs::read_to_string(path).await?;
112 let prd: Prd = serde_json::from_str(&content)?;
113 Ok(prd)
114 }
115
116 pub async fn save(&self, path: &PathBuf) -> anyhow::Result<()> {
118 let content = serde_json::to_string_pretty(self)?;
119 tokio::fs::write(path, content).await?;
120 Ok(())
121 }
122
123 pub fn next_story(&self) -> Option<&UserStory> {
125 self.user_stories
126 .iter()
127 .filter(|s| !s.passes)
128 .filter(|s| self.dependencies_met(&s.depends_on))
129 .min_by_key(|s| (s.priority, s.complexity))
130 }
131
132 fn dependencies_met(&self, deps: &[String]) -> bool {
134 deps.iter().all(|dep_id| {
135 self.user_stories
136 .iter()
137 .find(|s| s.id == *dep_id)
138 .map(|s| s.passes)
139 .unwrap_or(true)
140 })
141 }
142
143 pub fn passed_count(&self) -> usize {
145 self.user_stories.iter().filter(|s| s.passes).count()
146 }
147
148 pub fn is_complete(&self) -> bool {
150 self.user_stories.iter().all(|s| s.passes)
151 }
152
153 pub fn mark_passed(&mut self, story_id: &str) {
155 if let Some(story) = self.user_stories.iter_mut().find(|s| s.id == *story_id) {
156 story.passes = true;
157 }
158 }
159
160 pub fn ready_stories(&self) -> Vec<&UserStory> {
162 self.user_stories
163 .iter()
164 .filter(|s| !s.passes)
165 .filter(|s| self.dependencies_met(&s.depends_on))
166 .collect()
167 }
168
169 pub fn stages(&self) -> Vec<Vec<&UserStory>> {
172 let mut stages: Vec<Vec<&UserStory>> = Vec::new();
173 let mut completed: std::collections::HashSet<String> = self
174 .user_stories
175 .iter()
176 .filter(|s| s.passes)
177 .map(|s| s.id.clone())
178 .collect();
179
180 let mut remaining: Vec<&UserStory> =
181 self.user_stories.iter().filter(|s| !s.passes).collect();
182
183 while !remaining.is_empty() {
184 let (ready, not_ready): (Vec<_>, Vec<_>) = remaining
186 .into_iter()
187 .partition(|s| s.depends_on.iter().all(|dep| completed.contains(dep)));
188
189 if ready.is_empty() {
190 if !not_ready.is_empty() {
192 stages.push(not_ready);
193 }
194 break;
195 }
196
197 for story in &ready {
199 completed.insert(story.id.clone());
200 }
201
202 stages.push(ready);
203 remaining = not_ready;
204 }
205
206 stages
207 }
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct RalphState {
213 pub prd: Prd,
215
216 pub current_iteration: usize,
218
219 pub max_iterations: usize,
221
222 pub status: RalphStatus,
224
225 #[serde(default)]
227 pub progress_log: Vec<ProgressEntry>,
228
229 pub prd_path: PathBuf,
231
232 pub working_dir: PathBuf,
234}
235
236#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
238#[serde(rename_all = "snake_case")]
239pub enum RalphStatus {
240 Pending,
241 Running,
242 Completed,
243 MaxIterations,
244 Stopped,
245 QualityFailed,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct ProgressEntry {
251 pub story_id: String,
253
254 pub iteration: usize,
256
257 pub status: String,
259
260 #[serde(default)]
262 pub learnings: Vec<String>,
263
264 #[serde(default)]
266 pub files_changed: Vec<String>,
267
268 pub timestamp: String,
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct RalphConfig {
275 #[serde(default = "default_prd_path")]
277 pub prd_path: String,
278
279 #[serde(default = "default_max_iterations")]
281 pub max_iterations: usize,
282
283 #[serde(default = "default_progress_path")]
285 pub progress_path: String,
286
287 #[serde(default = "default_auto_commit")]
289 pub auto_commit: bool,
290
291 #[serde(default = "default_quality_checks_enabled")]
293 pub quality_checks_enabled: bool,
294
295 #[serde(default)]
297 pub model: Option<String>,
298
299 #[serde(default)]
301 pub use_rlm: bool,
302
303 #[serde(default = "default_parallel_enabled")]
305 pub parallel_enabled: bool,
306
307 #[serde(default = "default_max_concurrent_stories")]
309 pub max_concurrent_stories: usize,
310
311 #[serde(default = "default_worktree_enabled")]
313 pub worktree_enabled: bool,
314
315 #[serde(default = "default_story_timeout_secs")]
317 pub story_timeout_secs: u64,
318
319 #[serde(default = "default_conflict_timeout_secs")]
321 pub conflict_timeout_secs: u64,
322}
323
324fn default_prd_path() -> String {
325 "prd.json".to_string()
326}
327fn default_max_iterations() -> usize {
328 10
329}
330fn default_progress_path() -> String {
331 "progress.txt".to_string()
332}
333fn default_auto_commit() -> bool {
334 false
335}
336fn default_quality_checks_enabled() -> bool {
337 true
338}
339fn default_parallel_enabled() -> bool {
340 true
341}
342fn default_max_concurrent_stories() -> usize {
343 3
344}
345fn default_worktree_enabled() -> bool {
346 true
347}
348fn default_story_timeout_secs() -> u64 {
349 300
350}
351fn default_conflict_timeout_secs() -> u64 {
352 120
353}
354
355impl Default for RalphConfig {
356 fn default() -> Self {
357 Self {
358 prd_path: default_prd_path(),
359 max_iterations: default_max_iterations(),
360 progress_path: default_progress_path(),
361 auto_commit: default_auto_commit(),
362 quality_checks_enabled: default_quality_checks_enabled(),
363 model: None,
364 use_rlm: false,
365 parallel_enabled: default_parallel_enabled(),
366 max_concurrent_stories: default_max_concurrent_stories(),
367 worktree_enabled: default_worktree_enabled(),
368 story_timeout_secs: default_story_timeout_secs(),
369 conflict_timeout_secs: default_conflict_timeout_secs(),
370 }
371 }
372}