ralph_workflow/phases/
development.rs1use std::fmt::Write;
11
12use crate::files::llm_output_extraction::PlanElements;
13
14pub(crate) fn format_plan_as_markdown(elements: &PlanElements) -> String {
16 let mut result = String::new();
17
18 result.push_str("## Summary\n\n");
20 result.push_str(&elements.summary.context);
21 result.push_str("\n\n");
22
23 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 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 result.push_str("## Implementation Steps\n\n");
69 for step in &elements.steps {
70 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 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 if let Some(ref location) = step.location {
125 writeln!(result, "**Location:** {location}").unwrap();
126 }
127
128 if let Some(ref rationale) = step.rationale {
130 writeln!(result, "**Rationale:** {rationale}\n").unwrap();
131 }
132
133 result.push_str(&format_rich_content(&step.content));
135 result.push('\n');
136
137 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 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 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 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
213fn 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 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 result.push('|');
253 for _ in &first_row.cells {
254 result.push_str(" --- |");
255 }
256 result.push('\n');
257 }
258 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
285fn 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
302fn 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}