1use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(tag = "type", rename_all = "snake_case")]
13pub enum VerificationStep {
14 Shell {
16 #[serde(default)]
18 name: Option<String>,
19
20 command: String,
22
23 #[serde(default)]
25 cwd: Option<String>,
26
27 #[serde(default)]
29 expect_output_contains: Vec<String>,
30
31 #[serde(default)]
34 expect_files_glob: Vec<String>,
35 },
36
37 FileExists {
39 #[serde(default)]
40 name: Option<String>,
41 path: String,
43 #[serde(default)]
45 glob: bool,
46 },
47
48 Url {
50 #[serde(default)]
51 name: Option<String>,
52 url: String,
53 #[serde(default = "default_http_method")]
55 method: String,
56 #[serde(default = "default_http_status")]
58 expect_status: u16,
59 #[serde(default)]
61 expect_body_contains: Vec<String>,
62 #[serde(default = "default_http_timeout_secs")]
64 timeout_secs: u64,
65 },
66}
67
68fn default_http_method() -> String {
69 "GET".to_string()
70}
71fn default_http_status() -> u16 {
72 200
73}
74fn default_http_timeout_secs() -> u64 {
75 30
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct UserStory {
81 pub id: String,
83
84 pub title: String,
86
87 pub description: String,
89
90 #[serde(default)]
92 pub acceptance_criteria: Vec<String>,
93
94 #[serde(default)]
98 pub verification_steps: Vec<VerificationStep>,
99
100 #[serde(default)]
102 pub passes: bool,
103
104 #[serde(default = "default_priority")]
106 pub priority: u8,
107
108 #[serde(default, alias = "dependencies")]
110 pub depends_on: Vec<String>,
111
112 #[serde(default = "default_complexity")]
114 pub complexity: u8,
115}
116
117fn default_priority() -> u8 {
118 1
119}
120fn default_complexity() -> u8 {
121 3
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct Prd {
127 pub project: String,
129
130 pub feature: String,
132
133 #[serde(default)]
135 pub branch_name: String,
136
137 #[serde(default = "default_version")]
139 pub version: String,
140
141 #[serde(default)]
143 pub user_stories: Vec<UserStory>,
144
145 #[serde(default)]
147 pub technical_requirements: Vec<String>,
148
149 #[serde(default)]
151 pub quality_checks: QualityChecks,
152
153 #[serde(default)]
155 pub created_at: String,
156
157 #[serde(default)]
159 pub updated_at: String,
160}
161
162fn default_version() -> String {
163 "1.0".to_string()
164}
165
166#[derive(Debug, Clone, Default, Serialize, Deserialize)]
168pub struct QualityChecks {
169 #[serde(default)]
171 pub typecheck: Option<String>,
172
173 #[serde(default)]
175 pub test: Option<String>,
176
177 #[serde(default)]
179 pub lint: Option<String>,
180
181 #[serde(default)]
183 pub build: Option<String>,
184}
185
186impl Prd {
187 pub async fn load(path: &PathBuf) -> anyhow::Result<Self> {
189 let content = tokio::fs::read_to_string(path).await?;
190 let prd: Prd = serde_json::from_str(&content)?;
191 Ok(prd)
192 }
193
194 pub async fn save(&self, path: &PathBuf) -> anyhow::Result<()> {
196 let content = serde_json::to_string_pretty(self)?;
197 tokio::fs::write(path, content).await?;
198 Ok(())
199 }
200
201 pub fn next_story(&self) -> Option<&UserStory> {
203 self.user_stories
204 .iter()
205 .filter(|s| !s.passes)
206 .filter(|s| self.dependencies_met(&s.depends_on))
207 .min_by_key(|s| (s.priority, s.complexity))
208 }
209
210 fn dependencies_met(&self, deps: &[String]) -> bool {
212 deps.iter().all(|dep_id| {
213 self.user_stories
214 .iter()
215 .find(|s| s.id == *dep_id)
216 .map(|s| s.passes)
217 .unwrap_or(true)
218 })
219 }
220
221 pub fn passed_count(&self) -> usize {
223 self.user_stories.iter().filter(|s| s.passes).count()
224 }
225
226 pub fn is_complete(&self) -> bool {
228 self.user_stories.iter().all(|s| s.passes)
229 }
230
231 pub fn mark_passed(&mut self, story_id: &str) {
233 if let Some(story) = self.user_stories.iter_mut().find(|s| s.id == *story_id) {
234 story.passes = true;
235 }
236 }
237
238 pub fn ready_stories(&self) -> Vec<&UserStory> {
240 self.user_stories
241 .iter()
242 .filter(|s| !s.passes)
243 .filter(|s| self.dependencies_met(&s.depends_on))
244 .collect()
245 }
246
247 pub fn stages(&self) -> Vec<Vec<&UserStory>> {
250 let mut stages: Vec<Vec<&UserStory>> = Vec::new();
251 let mut completed: std::collections::HashSet<String> = self
252 .user_stories
253 .iter()
254 .filter(|s| s.passes)
255 .map(|s| s.id.clone())
256 .collect();
257
258 let mut remaining: Vec<&UserStory> =
259 self.user_stories.iter().filter(|s| !s.passes).collect();
260
261 while !remaining.is_empty() {
262 let (ready, not_ready): (Vec<_>, Vec<_>) = remaining
264 .into_iter()
265 .partition(|s| s.depends_on.iter().all(|dep| completed.contains(dep)));
266
267 if ready.is_empty() {
268 if !not_ready.is_empty() {
270 stages.push(not_ready);
271 }
272 break;
273 }
274
275 for story in &ready {
277 completed.insert(story.id.clone());
278 }
279
280 stages.push(ready);
281 remaining = not_ready;
282 }
283
284 stages
285 }
286}
287
288#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct RalphState {
291 pub prd: Prd,
293
294 pub current_iteration: usize,
296
297 pub max_iterations: usize,
299
300 pub status: RalphStatus,
302
303 #[serde(default)]
305 pub progress_log: Vec<ProgressEntry>,
306
307 pub prd_path: PathBuf,
309
310 pub working_dir: PathBuf,
312}
313
314#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
316#[serde(rename_all = "snake_case")]
317pub enum RalphStatus {
318 Pending,
319 Running,
320 Completed,
321 MaxIterations,
322 Stopped,
323 QualityFailed,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct ProgressEntry {
329 pub story_id: String,
331
332 pub iteration: usize,
334
335 pub status: String,
337
338 #[serde(default)]
340 pub learnings: Vec<String>,
341
342 #[serde(default)]
344 pub files_changed: Vec<String>,
345
346 pub timestamp: String,
348}
349
350#[derive(Debug, Clone, Serialize, Deserialize)]
352pub struct RalphConfig {
353 #[serde(default = "default_prd_path")]
355 pub prd_path: String,
356
357 #[serde(default = "default_max_iterations")]
359 pub max_iterations: usize,
360
361 #[serde(default = "default_progress_path")]
363 pub progress_path: String,
364
365 #[serde(default = "default_auto_commit")]
367 pub auto_commit: bool,
368
369 #[serde(default = "default_quality_checks_enabled")]
371 pub quality_checks_enabled: bool,
372
373 #[serde(default)]
375 pub model: Option<String>,
376
377 #[serde(default)]
379 pub use_rlm: bool,
380
381 #[serde(default = "default_parallel_enabled")]
383 pub parallel_enabled: bool,
384
385 #[serde(default = "default_max_concurrent_stories")]
387 pub max_concurrent_stories: usize,
388
389 #[serde(default = "default_worktree_enabled")]
391 pub worktree_enabled: bool,
392
393 #[serde(default = "default_story_timeout_secs")]
395 pub story_timeout_secs: u64,
396
397 #[serde(default = "default_max_steps_per_story")]
400 pub max_steps_per_story: usize,
401
402 #[serde(default = "default_conflict_timeout_secs")]
404 pub conflict_timeout_secs: u64,
405
406 #[serde(default)]
408 pub relay_enabled: bool,
409
410 #[serde(default = "default_relay_max_agents")]
412 pub relay_max_agents: usize,
413
414 #[serde(default = "default_relay_max_rounds")]
416 pub relay_max_rounds: usize,
417}
418
419fn default_prd_path() -> String {
420 "prd.json".to_string()
421}
422fn default_max_iterations() -> usize {
423 10
424}
425fn default_progress_path() -> String {
426 "progress.txt".to_string()
427}
428fn default_auto_commit() -> bool {
429 false
430}
431fn default_quality_checks_enabled() -> bool {
432 true
433}
434fn default_parallel_enabled() -> bool {
435 true
436}
437fn default_max_concurrent_stories() -> usize {
438 100
439}
440fn default_worktree_enabled() -> bool {
441 true
442}
443fn default_story_timeout_secs() -> u64 {
444 300
445}
446fn default_max_steps_per_story() -> usize {
447 50
448}
449fn default_conflict_timeout_secs() -> u64 {
450 120
451}
452fn default_relay_max_agents() -> usize {
453 8
454}
455fn default_relay_max_rounds() -> usize {
456 3
457}
458
459impl Default for RalphConfig {
460 fn default() -> Self {
461 Self {
462 prd_path: default_prd_path(),
463 max_iterations: default_max_iterations(),
464 progress_path: default_progress_path(),
465 auto_commit: default_auto_commit(),
466 quality_checks_enabled: default_quality_checks_enabled(),
467 model: None,
468 use_rlm: true,
469 parallel_enabled: default_parallel_enabled(),
470 max_concurrent_stories: default_max_concurrent_stories(),
471 worktree_enabled: default_worktree_enabled(),
472 story_timeout_secs: default_story_timeout_secs(),
473 max_steps_per_story: default_max_steps_per_story(),
474 conflict_timeout_secs: default_conflict_timeout_secs(),
475 relay_enabled: true,
476 relay_max_agents: default_relay_max_agents(),
477 relay_max_rounds: default_relay_max_rounds(),
478 }
479 }
480}