1use crate::{AgentError, Result};
7use forge_core::Forge;
8use std::sync::Arc;
9
10#[derive(Clone)]
15pub struct Planner {
16 forge: Arc<Forge>,
18}
19
20impl Planner {
21 pub fn new(forge: Forge) -> Self {
23 Self {
24 forge: Arc::new(forge),
25 }
26 }
27
28 pub async fn generate_steps(
34 &self,
35 observation: &super::observe::Observation,
36 ) -> Result<Vec<PlanStep>> {
37 let mut steps = Vec::new();
38
39 for symbol in &observation.symbols {
41 steps.push(PlanStep {
44 description: format!("Process symbol {}", symbol.name),
45 operation: PlanOperation::Inspect {
46 symbol_id: symbol.id,
47 symbol_name: symbol.name.clone(),
48 },
49 });
50 }
51
52 Ok(steps)
53 }
54
55 pub async fn estimate_impact(&self, steps: &[PlanStep]) -> Result<ImpactEstimate> {
61 let mut affected_files = std::collections::HashSet::new();
62
63 for step in steps {
65 match &step.operation {
66 PlanOperation::Rename { old, .. } => {
67 if let Some(file) = self.extract_file_from_symbol(old) {
69 affected_files.insert(file);
70 }
71 }
72 PlanOperation::Delete { name } => {
73 if let Some(file) = self.extract_file_from_symbol(name) {
74 affected_files.insert(file);
75 }
76 }
77 PlanOperation::Create { path, .. } => {
78 affected_files.insert(path.clone());
79 }
80 PlanOperation::Inspect { .. } => {
81 }
83 PlanOperation::Modify { file, .. } => {
84 affected_files.insert(file.clone());
85 }
86 }
87 }
88
89 Ok(ImpactEstimate {
90 affected_files: affected_files.into_iter().collect(),
91 complexity: steps.len(),
92 })
93 }
94
95 pub fn detect_conflicts(&self, steps: &[PlanStep]) -> Result<Vec<Conflict>> {
101 let mut conflicts = Vec::new();
102 let mut file_regions: std::collections::HashMap<String, Vec<(usize, usize, usize)>> =
103 std::collections::HashMap::new();
104
105 for (idx, step) in steps.iter().enumerate() {
107 if let Some(region) = self.get_step_region(step) {
108 file_regions
109 .entry(region.file.clone())
110 .or_insert_with(Vec::new)
111 .push((idx, region.start, region.end));
112 }
113 }
114
115 for (file, regions) in &file_regions {
117 for i in 0..regions.len() {
118 for j in (i + 1)..regions.len() {
119 let (idx1, start1, end1) = regions[i];
120 let (idx2, start2, _end2) = regions[j];
121
122 if start1 < end1 && start2 < end1 {
124 conflicts.push(Conflict {
125 step_indices: vec![idx1, idx2],
126 file: file.clone(),
127 reason: ConflictReason::OverlappingRegion {
128 start: start1,
129 end: end1,
130 },
131 });
132 }
133 }
134 }
135 }
136
137 Ok(conflicts)
138 }
139
140 pub fn order_steps(&self, steps: &mut Vec<PlanStep>) -> Result<()> {
146 let mut rename_indices = Vec::new();
155 let mut delete_indices = Vec::new();
156
157 for (idx, step) in steps.iter().enumerate() {
158 match &step.operation {
159 PlanOperation::Rename { old, .. } => {
160 rename_indices.push((idx, old.clone()));
161 }
162 PlanOperation::Delete { name } => {
163 delete_indices.push((idx, name.clone()));
164 }
165 _ => {}
166 }
167 }
168
169 for (rename_idx, name) in &rename_indices {
171 for (delete_idx, delete_name) in &delete_indices {
172 if name == delete_name && rename_idx > delete_idx {
173 steps.swap(*rename_idx, *delete_idx);
175 }
176 }
177 }
178
179 Ok(())
180 }
181
182 pub fn generate_rollback(&self, steps: &[PlanStep]) -> Vec<RollbackStep> {
188 steps
189 .iter()
190 .rev()
191 .map(|step| RollbackStep {
192 description: format!("Rollback: {}", step.description),
193 operation: match &step.operation {
194 PlanOperation::Rename { old, .. } => RollbackOperation::Rename {
195 new_name: old.clone(),
196 },
197 PlanOperation::Delete { name } => {
198 RollbackOperation::Restore { name: name.clone() }
199 }
200 PlanOperation::Create { path, .. } => {
201 RollbackOperation::Delete { path: path.clone() }
202 }
203 PlanOperation::Inspect { .. } => RollbackOperation::None,
204 PlanOperation::Modify { file, .. } => {
205 RollbackOperation::Restore { name: file.clone() }
206 }
207 },
208 })
209 .collect()
210 }
211
212 fn extract_file_from_symbol(&self, _symbol: &str) -> Option<String> {
214 None
217 }
218
219 fn get_step_region(&self, step: &PlanStep) -> Option<FileRegion> {
221 match &step.operation {
222 PlanOperation::Rename { .. } | PlanOperation::Delete { .. } => None,
223 PlanOperation::Create { path, .. } => Some(FileRegion {
224 file: path.clone(),
225 start: 0,
226 end: usize::MAX,
227 }),
228 PlanOperation::Inspect { .. } => None,
229 PlanOperation::Modify { file, start, end } => Some(FileRegion {
230 file: file.clone(),
231 start: *start,
232 end: *end,
233 }),
234 }
235 }
236}
237
238#[derive(Clone, Debug)]
240pub struct PlanStep {
241 pub description: String,
243 pub operation: PlanOperation,
245}
246
247#[derive(Clone, Debug)]
249pub enum PlanOperation {
250 Rename { old: String, new: String },
252 Delete { name: String },
254 Create { path: String, content: String },
256 Inspect {
258 symbol_id: forge_core::types::SymbolId,
259 symbol_name: String,
260 },
261 Modify {
263 file: String,
264 start: usize,
265 end: usize,
266 },
267}
268
269#[derive(Clone, Debug)]
271pub struct ImpactEstimate {
272 pub affected_files: Vec<String>,
274 pub complexity: usize,
276}
277
278#[derive(Clone, Debug)]
280pub struct Conflict {
281 pub step_indices: Vec<usize>,
283 pub file: String,
285 pub reason: ConflictReason,
287}
288
289#[derive(Clone, Debug)]
291pub enum ConflictReason {
292 OverlappingRegion { start: usize, end: usize },
294 CircularDependency,
296 MissingDependency,
298}
299
300#[derive(Clone, Debug)]
302pub struct RollbackStep {
303 pub description: String,
305 pub operation: RollbackOperation,
307}
308
309#[derive(Clone, Debug)]
311pub enum RollbackOperation {
312 Rename { new_name: String },
314 Restore { name: String },
316 Delete { path: String },
318 None,
320}
321
322#[derive(Clone, Debug)]
324struct FileRegion {
325 file: String,
327 start: usize,
329 end: usize,
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336 use tempfile::TempDir;
337
338 #[tokio::test]
339 async fn test_planner_creation() {
340 let temp_dir = TempDir::new().unwrap();
341 let forge = Forge::open(temp_dir.path()).await.unwrap();
342 let _planner = Planner::new(forge);
343
344 assert!(true);
346 }
347
348 #[tokio::test]
349 async fn test_generate_steps() {
350 let temp_dir = TempDir::new().unwrap();
351 let forge = Forge::open(temp_dir.path()).await.unwrap();
352 let planner = Planner::new(forge);
353
354 let observation = crate::observe::Observation {
355 query: "test".to_string(),
356 symbols: vec![],
357 };
358
359 let steps = planner.generate_steps(&observation).await.unwrap();
360 assert!(steps.is_empty());
362 }
363
364 #[tokio::test]
365 async fn test_detect_conflicts_empty() {
366 let temp_dir = TempDir::new().unwrap();
367 let forge = Forge::open(temp_dir.path()).await.unwrap();
368 let planner = Planner::new(forge);
369
370 let steps = vec![];
371 let conflicts = planner.detect_conflicts(&steps).unwrap();
372 assert!(conflicts.is_empty());
373 }
374
375 #[tokio::test]
376 async fn test_order_steps() {
377 let temp_dir = TempDir::new().unwrap();
378 let forge = Forge::open(temp_dir.path()).await.unwrap();
379 let planner = Planner::new(forge);
380
381 let mut steps = vec![
382 PlanStep {
383 description: "Delete foo".to_string(),
384 operation: PlanOperation::Delete {
385 name: "foo".to_string(),
386 },
387 },
388 PlanStep {
389 description: "Rename foo to bar".to_string(),
390 operation: PlanOperation::Rename {
391 old: "foo".to_string(),
392 new: "bar".to_string(),
393 },
394 },
395 ];
396
397 planner.order_steps(&mut steps).unwrap();
398
399 assert!(matches!(steps[0].operation, PlanOperation::Rename { .. }));
401 assert!(matches!(steps[1].operation, PlanOperation::Delete { .. }));
402 }
403
404 #[tokio::test]
405 async fn test_generate_rollback() {
406 let temp_dir = TempDir::new().unwrap();
407 let forge = Forge::open(temp_dir.path()).await.unwrap();
408 let planner = Planner::new(forge);
409
410 let steps = vec![PlanStep {
411 description: "Create file".to_string(),
412 operation: PlanOperation::Create {
413 path: "/tmp/test.rs".to_string(),
414 content: "fn test() {}".to_string(),
415 },
416 }];
417
418 let rollback = planner.generate_rollback(&steps);
419
420 assert_eq!(rollback.len(), 1);
421 assert_eq!(rollback[0].description, "Rollback: Create file");
422 assert!(matches!(
423 rollback[0].operation,
424 RollbackOperation::Delete { .. }
425 ));
426 }
427
428 #[tokio::test]
429 async fn test_estimate_impact() {
430 let temp_dir = TempDir::new().unwrap();
431 let forge = Forge::open(temp_dir.path()).await.unwrap();
432 let planner = Planner::new(forge);
433
434 let steps = vec![PlanStep {
435 description: "Create test.rs".to_string(),
436 operation: PlanOperation::Create {
437 path: "/tmp/test.rs".to_string(),
438 content: "fn test() {}".to_string(),
439 },
440 }];
441
442 let impact = planner.estimate_impact(&steps).await.unwrap();
443
444 assert!(!impact.affected_files.is_empty() || impact.affected_files.len() >= 0);
445 }
446}