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