1use crate::Result;
7
8#[derive(Clone, Default)]
13pub struct Planner {}
14
15impl Planner {
16 pub fn new() -> Self {
18 Self::default()
19 }
20
21 pub async fn generate_steps(
27 &self,
28 observation: &super::observe::Observation,
29 ) -> Result<Vec<PlanStep>> {
30 let mut steps = Vec::new();
31
32 for symbol in &observation.symbols {
34 steps.push(PlanStep {
37 description: format!("Process symbol {}", symbol.name),
38 operation: PlanOperation::Inspect {
39 symbol_id: symbol.id,
40 symbol_name: symbol.name.clone(),
41 },
42 });
43 }
44
45 Ok(steps)
46 }
47
48 pub async fn estimate_impact(&self, steps: &[PlanStep]) -> Result<ImpactEstimate> {
54 let mut affected_files = std::collections::HashSet::new();
55
56 for step in steps {
58 match &step.operation {
59 PlanOperation::Rename { old, .. } => {
60 if let Some(file) = self.extract_file_from_symbol(old) {
62 affected_files.insert(file);
63 }
64 }
65 PlanOperation::Delete { name } => {
66 if let Some(file) = self.extract_file_from_symbol(name) {
67 affected_files.insert(file);
68 }
69 }
70 PlanOperation::Create { path, .. } => {
71 affected_files.insert(path.clone());
72 }
73 PlanOperation::Inspect { .. } => {
74 }
76 PlanOperation::Modify { file, .. } => {
77 affected_files.insert(file.clone());
78 }
79 }
80 }
81
82 Ok(ImpactEstimate {
83 affected_files: affected_files.into_iter().collect(),
84 complexity: steps.len(),
85 })
86 }
87
88 pub fn detect_conflicts(&self, steps: &[PlanStep]) -> Result<Vec<Conflict>> {
94 let mut conflicts = Vec::new();
95 let mut file_regions: std::collections::HashMap<String, Vec<(usize, usize, usize)>> =
96 std::collections::HashMap::new();
97
98 for (idx, step) in steps.iter().enumerate() {
100 if let Some(region) = self.get_step_region(step) {
101 file_regions
102 .entry(region.file.clone())
103 .or_insert_with(Vec::new)
104 .push((idx, region.start, region.end));
105 }
106 }
107
108 for (file, regions) in &file_regions {
110 for i in 0..regions.len() {
111 for j in (i + 1)..regions.len() {
112 let (idx1, start1, end1) = regions[i];
113 let (idx2, start2, _end2) = regions[j];
114
115 if start1 < end1 && start2 < end1 {
117 conflicts.push(Conflict {
118 step_indices: vec![idx1, idx2],
119 file: file.clone(),
120 reason: ConflictReason::OverlappingRegion {
121 start: start1,
122 end: end1,
123 },
124 });
125 }
126 }
127 }
128 }
129
130 Ok(conflicts)
131 }
132
133 pub fn order_steps(&self, steps: &mut Vec<PlanStep>) -> Result<()> {
139 let mut rename_indices = Vec::new();
148 let mut delete_indices = Vec::new();
149
150 for (idx, step) in steps.iter().enumerate() {
151 match &step.operation {
152 PlanOperation::Rename { old, .. } => {
153 rename_indices.push((idx, old.clone()));
154 }
155 PlanOperation::Delete { name } => {
156 delete_indices.push((idx, name.clone()));
157 }
158 _ => {}
159 }
160 }
161
162 for (rename_idx, name) in &rename_indices {
164 for (delete_idx, delete_name) in &delete_indices {
165 if name == delete_name && rename_idx > delete_idx {
166 steps.swap(*rename_idx, *delete_idx);
168 }
169 }
170 }
171
172 Ok(())
173 }
174
175 pub fn generate_rollback(&self, steps: &[PlanStep]) -> Vec<RollbackStep> {
181 steps
182 .iter()
183 .rev()
184 .map(|step| RollbackStep {
185 description: format!("Rollback: {}", step.description),
186 operation: match &step.operation {
187 PlanOperation::Rename { old, .. } => RollbackOperation::Rename {
188 new_name: old.clone(),
189 },
190 PlanOperation::Delete { name } => {
191 RollbackOperation::Restore { name: name.clone() }
192 }
193 PlanOperation::Create { path, .. } => {
194 RollbackOperation::Delete { path: path.clone() }
195 }
196 PlanOperation::Inspect { .. } => RollbackOperation::None,
197 PlanOperation::Modify { file, .. } => {
198 RollbackOperation::Restore { name: file.clone() }
199 }
200 },
201 })
202 .collect()
203 }
204
205 fn extract_file_from_symbol(&self, _symbol: &str) -> Option<String> {
207 None
210 }
211
212 fn get_step_region(&self, step: &PlanStep) -> Option<FileRegion> {
214 match &step.operation {
215 PlanOperation::Rename { .. } | PlanOperation::Delete { .. } => None,
216 PlanOperation::Create { path, .. } => Some(FileRegion {
217 file: path.clone(),
218 start: 0,
219 end: usize::MAX,
220 }),
221 PlanOperation::Inspect { .. } => None,
222 PlanOperation::Modify { file, start, end } => Some(FileRegion {
223 file: file.clone(),
224 start: *start,
225 end: *end,
226 }),
227 }
228 }
229}
230
231#[derive(Clone, Debug)]
233pub struct PlanStep {
234 pub description: String,
236 pub operation: PlanOperation,
238}
239
240#[derive(Clone, Debug)]
242pub enum PlanOperation {
243 Rename { old: String, new: String },
245 Delete { name: String },
247 Create { path: String, content: String },
249 Inspect {
251 symbol_id: forge_core::types::SymbolId,
252 symbol_name: String,
253 },
254 Modify {
256 file: String,
257 start: usize,
258 end: usize,
259 },
260}
261
262#[derive(Clone, Debug)]
264pub struct ImpactEstimate {
265 pub affected_files: Vec<String>,
267 pub complexity: usize,
269}
270
271#[derive(Clone, Debug)]
273pub struct Conflict {
274 pub step_indices: Vec<usize>,
276 pub file: String,
278 pub reason: ConflictReason,
280}
281
282#[derive(Clone, Debug)]
284pub enum ConflictReason {
285 OverlappingRegion { start: usize, end: usize },
287 CircularDependency,
289 MissingDependency,
291}
292
293#[derive(Clone, Debug)]
295pub struct RollbackStep {
296 pub description: String,
298 pub operation: RollbackOperation,
300}
301
302#[derive(Clone, Debug)]
304pub enum RollbackOperation {
305 Rename { new_name: String },
307 Restore { name: String },
309 Delete { path: String },
311 None,
313}
314
315#[derive(Clone, Debug)]
317struct FileRegion {
318 file: String,
320 start: usize,
322 end: usize,
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 #[tokio::test]
331 async fn test_planner_creation() {
332 let _planner = Planner::new();
333
334 assert!(true);
336 }
337
338 #[tokio::test]
339 async fn test_generate_steps() {
340 let planner = Planner::new();
341
342 let observation = crate::observe::Observation {
343 query: "test".to_string(),
344 symbols: vec![],
345 };
346
347 let steps = planner.generate_steps(&observation).await.unwrap();
348 assert!(steps.is_empty());
350 }
351
352 #[tokio::test]
353 async fn test_detect_conflicts_empty() {
354 let planner = Planner::new();
355
356 let steps = vec![];
357 let conflicts = planner.detect_conflicts(&steps).unwrap();
358 assert!(conflicts.is_empty());
359 }
360
361 #[tokio::test]
362 async fn test_order_steps() {
363 let planner = Planner::new();
364
365 let mut steps = vec![
366 PlanStep {
367 description: "Delete foo".to_string(),
368 operation: PlanOperation::Delete {
369 name: "foo".to_string(),
370 },
371 },
372 PlanStep {
373 description: "Rename foo to bar".to_string(),
374 operation: PlanOperation::Rename {
375 old: "foo".to_string(),
376 new: "bar".to_string(),
377 },
378 },
379 ];
380
381 planner.order_steps(&mut steps).unwrap();
382
383 assert!(matches!(steps[0].operation, PlanOperation::Rename { .. }));
385 assert!(matches!(steps[1].operation, PlanOperation::Delete { .. }));
386 }
387
388 #[tokio::test]
389 async fn test_generate_rollback() {
390 let planner = Planner::new();
391
392 let steps = vec![PlanStep {
393 description: "Create file".to_string(),
394 operation: PlanOperation::Create {
395 path: "/tmp/test.rs".to_string(),
396 content: "fn test() {}".to_string(),
397 },
398 }];
399
400 let rollback = planner.generate_rollback(&steps);
401
402 assert_eq!(rollback.len(), 1);
403 assert_eq!(rollback[0].description, "Rollback: Create file");
404 assert!(matches!(
405 rollback[0].operation,
406 RollbackOperation::Delete { .. }
407 ));
408 }
409
410 #[tokio::test]
411 async fn test_estimate_impact() {
412 let planner = Planner::new();
413
414 let steps = vec![PlanStep {
415 description: "Create test.rs".to_string(),
416 operation: PlanOperation::Create {
417 path: "/tmp/test.rs".to_string(),
418 content: "fn test() {}".to_string(),
419 },
420 }];
421
422 let impact = planner.estimate_impact(&steps).await.unwrap();
423
424 assert!(!impact.affected_files.is_empty() || impact.affected_files.len() >= 0);
425 }
426}