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