brainwires_core/
plan_parser.rs1use crate::task::{Task, TaskPriority};
7use regex::Regex;
8use std::sync::LazyLock;
9
10#[derive(Debug, Clone)]
12pub struct ParsedStep {
13 pub number: usize,
15 pub description: String,
17 pub indent_level: usize,
19 pub is_priority: bool,
21}
22
23pub fn parse_plan_steps(content: &str) -> Vec<ParsedStep> {
25 let mut steps = Vec::new();
26
27 static NUMBERED_RE: LazyLock<Regex> =
35 LazyLock::new(|| Regex::new(r"^(\s*)(\d+)[.)]\s*(.+)$").expect("valid regex"));
36 static STEP_COLON_RE: LazyLock<Regex> =
37 LazyLock::new(|| Regex::new(r"^(\s*)(?:Step\s+)?(\d+):\s*(.+)$").expect("valid regex"));
38 static BULLET_RE: LazyLock<Regex> =
39 LazyLock::new(|| Regex::new(r"^(\s*)[-*]\s+(.+)$").expect("valid regex"));
40 let numbered_re = &*NUMBERED_RE;
41 let step_colon_re = &*STEP_COLON_RE;
42 let bullet_re = &*BULLET_RE;
43
44 let mut current_number = 0;
45
46 for line in content.lines() {
47 let trimmed = line.trim();
48 if trimmed.is_empty() {
49 continue;
50 }
51
52 if let Some(caps) = numbered_re.captures(line) {
54 let indent = caps.get(1).map(|m| m.as_str().len()).unwrap_or(0);
55 let _num: usize = caps
56 .get(2)
57 .expect("group 2 always present in match")
58 .as_str()
59 .parse()
60 .unwrap_or(0);
61 let desc = caps
62 .get(3)
63 .expect("group 3 always present in match")
64 .as_str()
65 .trim();
66
67 current_number += 1;
68 let indent_level = indent / 2; steps.push(ParsedStep {
71 number: current_number,
72 description: desc.to_string(),
73 indent_level,
74 is_priority: desc.to_lowercase().contains("important")
75 || desc.to_lowercase().contains("critical")
76 || desc.contains("!"),
77 });
78 continue;
79 }
80
81 if let Some(caps) = step_colon_re.captures(line) {
83 let indent = caps.get(1).map(|m| m.as_str().len()).unwrap_or(0);
84 let _num: usize = caps
85 .get(2)
86 .expect("group 2 always present in match")
87 .as_str()
88 .parse()
89 .unwrap_or(0);
90 let desc = caps
91 .get(3)
92 .expect("group 3 always present in match")
93 .as_str()
94 .trim();
95
96 current_number += 1;
97 let indent_level = indent / 2;
98
99 steps.push(ParsedStep {
100 number: current_number,
101 description: desc.to_string(),
102 indent_level,
103 is_priority: desc.to_lowercase().contains("important")
104 || desc.to_lowercase().contains("critical"),
105 });
106 continue;
107 }
108
109 if let Some(caps) = bullet_re.captures(line) {
111 let indent = caps.get(1).map(|m| m.as_str().len()).unwrap_or(0);
112 let desc = caps
113 .get(2)
114 .expect("group 2 always present in match")
115 .as_str()
116 .trim();
117
118 if desc.starts_with("Note:") || desc.starts_with("Warning:") {
120 continue;
121 }
122
123 if desc.len() > 10
125 && (desc.to_lowercase().contains("create")
126 || desc.to_lowercase().contains("add")
127 || desc.to_lowercase().contains("implement")
128 || desc.to_lowercase().contains("update")
129 || desc.to_lowercase().contains("modify")
130 || desc.to_lowercase().contains("configure")
131 || desc.to_lowercase().contains("set up")
132 || desc.to_lowercase().contains("install")
133 || desc.to_lowercase().contains("test")
134 || desc.to_lowercase().contains("verify")
135 || desc.to_lowercase().contains("check")
136 || desc.to_lowercase().contains("review")
137 || desc.to_lowercase().contains("fix")
138 || desc.to_lowercase().contains("remove")
139 || desc.to_lowercase().contains("delete"))
140 {
141 current_number += 1;
142 let indent_level = indent / 2;
143
144 steps.push(ParsedStep {
145 number: current_number,
146 description: desc.to_string(),
147 indent_level,
148 is_priority: false,
149 });
150 }
151 }
152 }
153
154 steps
155}
156
157pub fn steps_to_tasks(steps: &[ParsedStep], plan_id: &str) -> Vec<Task> {
159 let mut tasks = Vec::new();
160 let mut parent_stack: Vec<String> = Vec::new();
161
162 for step in steps {
163 let task_id = format!("{}-step-{}", &plan_id[..8.min(plan_id.len())], step.number);
164
165 let priority = if step.is_priority {
166 TaskPriority::High
167 } else {
168 TaskPriority::Normal
169 };
170
171 let mut task = Task::new_for_plan(
172 task_id.clone(),
173 step.description.clone(),
174 plan_id.to_string(),
175 );
176 task.priority = priority;
177
178 if step.indent_level == 0 {
180 parent_stack.clear();
182 parent_stack.push(task_id.clone());
183 } else if step.indent_level <= parent_stack.len() {
184 parent_stack.truncate(step.indent_level);
186 if let Some(parent_id) = parent_stack.last() {
187 task.parent_id = Some(parent_id.clone());
188 }
189 parent_stack.push(task_id.clone());
190 } else {
191 if let Some(parent_id) = parent_stack.last() {
193 task.parent_id = Some(parent_id.clone());
194 }
195 parent_stack.push(task_id.clone());
196 }
197
198 if !tasks.is_empty() && step.indent_level == 0 {
200 let prev_task: &Task = &tasks[tasks.len() - 1];
202 if prev_task.parent_id.is_none() {
203 task.depends_on.push(prev_task.id.clone());
204 }
205 }
206
207 tasks.push(task);
208 }
209
210 let task_ids: Vec<_> = tasks.iter().map(|t| t.id.clone()).collect();
212 for i in 0..tasks.len() {
213 if let Some(ref parent_id) = tasks[i].parent_id.clone() {
214 for task in tasks.iter_mut() {
216 if task.id == *parent_id {
217 task.children.push(task_ids[i].clone());
218 break;
219 }
220 }
221 }
222 }
223
224 tasks
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn test_parse_numbered_steps() {
233 let content = r#"
2341. Create the user model
2352. Add authentication endpoints
2363. Implement JWT token handling
237"#;
238 let steps = parse_plan_steps(content);
239 assert_eq!(steps.len(), 3);
240 assert_eq!(steps[0].description, "Create the user model");
241 assert_eq!(steps[1].description, "Add authentication endpoints");
242 }
243
244 #[test]
245 fn test_parse_step_colon_format() {
246 let content = r#"
247Step 1: Initialize the project
248Step 2: Configure dependencies
249"#;
250 let steps = parse_plan_steps(content);
251 assert_eq!(steps.len(), 2);
252 assert_eq!(steps[0].description, "Initialize the project");
253 }
254
255 #[test]
256 fn test_parse_indented_steps() {
257 let content = r#"
2581. Setup phase
259 1. Install dependencies
260 2. Configure environment
2612. Implementation phase
262"#;
263 let steps = parse_plan_steps(content);
264 assert_eq!(steps.len(), 4);
265 }
267
268 #[test]
269 fn test_steps_to_tasks() {
270 let content = "1. First step\n2. Second step";
271 let steps = parse_plan_steps(content);
272 let tasks = steps_to_tasks(&steps, "plan-12345678");
273
274 assert_eq!(tasks.len(), 2);
275 assert!(tasks[0].plan_id.is_some());
276 assert_eq!(tasks[0].plan_id.as_ref().unwrap(), "plan-12345678");
277 }
278
279 #[test]
280 fn test_priority_detection() {
281 let content = "1. Important: Fix critical bug!\n2. Normal task";
282 let steps = parse_plan_steps(content);
283
284 assert!(steps[0].is_priority);
285 assert!(!steps[1].is_priority);
286 }
287}