Skip to main content

ralph_workflow/phases/
development.rs

1//! Development phase execution.
2//!
3//! This module handles the development phase of the Ralph pipeline, which consists
4//! of iterative planning and execution cycles. Each iteration:
5//! 1. Creates a PLAN.md from PROMPT.md
6//! 2. Executes the plan
7//! 3. Deletes PLAN.md
8//! 4. Optionally runs fast checks
9
10use crate::files::llm_output_extraction::PlanElements;
11
12/// Format plan elements as markdown for PLAN.md.
13pub(crate) fn format_plan_as_markdown(elements: &PlanElements) -> String {
14    let mut result = String::new();
15
16    // Summary section
17    result.push_str("## Summary\n\n");
18    result.push_str(&elements.summary.context);
19    result.push_str("\n\n");
20
21    // Scope items
22    result.push_str("### Scope\n\n");
23    for item in &elements.summary.scope_items {
24        if let Some(ref count) = item.count {
25            result.push_str(&format!("- **{}** {}", count, item.description));
26        } else {
27            result.push_str(&format!("- {}", item.description));
28        }
29        if let Some(ref category) = item.category {
30            result.push_str(&format!(" ({})", category));
31        }
32        result.push('\n');
33    }
34    result.push('\n');
35
36    // Implementation steps
37    result.push_str("## Implementation Steps\n\n");
38    for step in &elements.steps {
39        // Step header
40        let step_type_str = match step.step_type {
41            crate::files::llm_output_extraction::xsd_validation_plan::StepType::FileChange => {
42                "file-change"
43            }
44            crate::files::llm_output_extraction::xsd_validation_plan::StepType::Action => "action",
45            crate::files::llm_output_extraction::xsd_validation_plan::StepType::Research => {
46                "research"
47            }
48        };
49        let priority_str = step.priority.map_or(String::new(), |p| {
50            format!(
51                " [{}]",
52                match p {
53                    crate::files::llm_output_extraction::xsd_validation_plan::Priority::Critical =>
54                        "critical",
55                    crate::files::llm_output_extraction::xsd_validation_plan::Priority::High =>
56                        "high",
57                    crate::files::llm_output_extraction::xsd_validation_plan::Priority::Medium =>
58                        "medium",
59                    crate::files::llm_output_extraction::xsd_validation_plan::Priority::Low =>
60                        "low",
61                }
62            )
63        });
64
65        result.push_str(&format!(
66            "### Step {} ({}){}:  {}\n\n",
67            step.number, step_type_str, priority_str, step.title
68        ));
69
70        // Target files
71        if !step.target_files.is_empty() {
72            result.push_str("**Target Files:**\n");
73            for tf in &step.target_files {
74                let action_str = match tf.action {
75                    crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Create => {
76                        "create"
77                    }
78                    crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Modify => {
79                        "modify"
80                    }
81                    crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Delete => {
82                        "delete"
83                    }
84                };
85                result.push_str(&format!("- `{}` ({})\n", tf.path, action_str));
86            }
87            result.push('\n');
88        }
89
90        // Location
91        if let Some(ref location) = step.location {
92            result.push_str(&format!("**Location:** {}\n\n", location));
93        }
94
95        // Rationale
96        if let Some(ref rationale) = step.rationale {
97            result.push_str(&format!("**Rationale:** {}\n\n", rationale));
98        }
99
100        // Content
101        result.push_str(&format_rich_content(&step.content));
102        result.push('\n');
103
104        // Dependencies
105        if !step.depends_on.is_empty() {
106            result.push_str("**Depends on:** ");
107            let deps: Vec<String> = step
108                .depends_on
109                .iter()
110                .map(|d| format!("Step {}", d))
111                .collect();
112            result.push_str(&deps.join(", "));
113            result.push_str("\n\n");
114        }
115    }
116
117    // Critical files
118    result.push_str("## Critical Files\n\n");
119    result.push_str("### Primary Files\n\n");
120    for pf in &elements.critical_files.primary_files {
121        let action_str = match pf.action {
122            crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Create => {
123                "create"
124            }
125            crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Modify => {
126                "modify"
127            }
128            crate::files::llm_output_extraction::xsd_validation_plan::FileAction::Delete => {
129                "delete"
130            }
131        };
132        if let Some(ref est) = pf.estimated_changes {
133            result.push_str(&format!("- `{}` ({}) - {}\n", pf.path, action_str, est));
134        } else {
135            result.push_str(&format!("- `{}` ({})\n", pf.path, action_str));
136        }
137    }
138    result.push('\n');
139
140    if !elements.critical_files.reference_files.is_empty() {
141        result.push_str("### Reference Files\n\n");
142        for rf in &elements.critical_files.reference_files {
143            result.push_str(&format!("- `{}` - {}\n", rf.path, rf.purpose));
144        }
145        result.push('\n');
146    }
147
148    // Risks and mitigations
149    result.push_str("## Risks & Mitigations\n\n");
150    for rp in &elements.risks_mitigations {
151        let severity_str = rp.severity.map_or(String::new(), |s| {
152            format!(
153                " [{}]",
154                match s {
155                    crate::files::llm_output_extraction::xsd_validation_plan::Severity::Low =>
156                        "low",
157                    crate::files::llm_output_extraction::xsd_validation_plan::Severity::Medium =>
158                        "medium",
159                    crate::files::llm_output_extraction::xsd_validation_plan::Severity::High =>
160                        "high",
161                    crate::files::llm_output_extraction::xsd_validation_plan::Severity::Critical =>
162                        "critical",
163                }
164            )
165        });
166        result.push_str(&format!("**Risk{}:** {}\n", severity_str, rp.risk));
167        result.push_str(&format!("**Mitigation:** {}\n\n", rp.mitigation));
168    }
169
170    // Verification strategy
171    result.push_str("## Verification Strategy\n\n");
172    for (i, v) in elements.verification_strategy.iter().enumerate() {
173        result.push_str(&format!("{}. **{}**\n", i + 1, v.method));
174        result.push_str(&format!("   Expected: {}\n\n", v.expected_outcome));
175    }
176
177    result
178}
179
180/// Format rich content elements to markdown.
181fn format_rich_content(
182    content: &crate::files::llm_output_extraction::xsd_validation_plan::RichContent,
183) -> String {
184    use crate::files::llm_output_extraction::xsd_validation_plan::ContentElement;
185
186    let mut result = String::new();
187
188    for element in &content.elements {
189        match element {
190            ContentElement::Paragraph(p) => {
191                result.push_str(&format_inline_content(&p.content));
192                result.push_str("\n\n");
193            }
194            ContentElement::CodeBlock(cb) => {
195                let lang = cb.language.as_deref().unwrap_or("");
196                result.push_str(&format!("```{}\n", lang));
197                result.push_str(&cb.content);
198                if !cb.content.ends_with('\n') {
199                    result.push('\n');
200                }
201                result.push_str("```\n\n");
202            }
203            ContentElement::Table(t) => {
204                if let Some(ref caption) = t.caption {
205                    result.push_str(&format!("**{}**\n\n", caption));
206                }
207                // Header row
208                if !t.columns.is_empty() {
209                    result.push_str("| ");
210                    result.push_str(&t.columns.join(" | "));
211                    result.push_str(" |\n");
212                    result.push('|');
213                    for _ in &t.columns {
214                        result.push_str(" --- |");
215                    }
216                    result.push('\n');
217                } else if let Some(first_row) = t.rows.first() {
218                    // Infer column count from first row
219                    result.push('|');
220                    for _ in &first_row.cells {
221                        result.push_str(" --- |");
222                    }
223                    result.push('\n');
224                }
225                // Data rows
226                for row in &t.rows {
227                    result.push_str("| ");
228                    let cells: Vec<String> = row
229                        .cells
230                        .iter()
231                        .map(|c| format_inline_content(&c.content))
232                        .collect();
233                    result.push_str(&cells.join(" | "));
234                    result.push_str(" |\n");
235                }
236                result.push('\n');
237            }
238            ContentElement::List(l) => {
239                result.push_str(&format_list(l, 0));
240                result.push('\n');
241            }
242            ContentElement::Heading(h) => {
243                let prefix = "#".repeat(h.level as usize);
244                result.push_str(&format!("{} {}\n\n", prefix, h.text));
245            }
246        }
247    }
248
249    result
250}
251
252/// Format inline content elements.
253fn format_inline_content(
254    content: &[crate::files::llm_output_extraction::xsd_validation_plan::InlineElement],
255) -> String {
256    use crate::files::llm_output_extraction::xsd_validation_plan::InlineElement;
257
258    content
259        .iter()
260        .map(|e| match e {
261            InlineElement::Text(s) => s.clone(),
262            InlineElement::Emphasis(s) => format!("**{}**", s),
263            InlineElement::Code(s) => format!("`{}`", s),
264            InlineElement::Link { href, text } => format!("[{}]({})", text, href),
265        })
266        .collect::<Vec<_>>()
267        .join("")
268}
269
270/// Format a list element with proper indentation.
271fn format_list(
272    list: &crate::files::llm_output_extraction::xsd_validation_plan::List,
273    indent: usize,
274) -> String {
275    use crate::files::llm_output_extraction::xsd_validation_plan::ListType;
276
277    let mut result = String::new();
278    let indent_str = "  ".repeat(indent);
279
280    for (i, item) in list.items.iter().enumerate() {
281        let marker = match list.list_type {
282            ListType::Ordered => format!("{}. ", i + 1),
283            ListType::Unordered => "- ".to_string(),
284        };
285
286        result.push_str(&indent_str);
287        result.push_str(&marker);
288        result.push_str(&format_inline_content(&item.content));
289        result.push('\n');
290
291        if let Some(ref nested) = item.nested_list {
292            result.push_str(&format_list(nested, indent + 1));
293        }
294    }
295
296    result
297}