1use crate::error::{ExecutionError, ExecutionResult};
4use crate::models::{
5 ComplexityLevel, ExecutionPlan, ExecutionStep, RiskFactor, RiskLevel, RiskScore, StepAction,
6};
7use ricecoder_storage::PathResolver;
8use std::collections::HashMap;
9use std::path::Path;
10use std::time::Duration;
11use uuid::Uuid;
12
13pub struct PlanBuilder {
18 name: String,
20 steps: Vec<ExecutionStep>,
22 dependencies: HashMap<String, Vec<String>>,
24 critical_files: Vec<String>,
26}
27
28impl PlanBuilder {
29 pub fn new(name: String) -> Self {
31 Self {
32 name,
33 steps: Vec::new(),
34 dependencies: HashMap::new(),
35 critical_files: vec![
36 "Cargo.toml".to_string(),
37 "package.json".to_string(),
38 "setup.py".to_string(),
39 "pyproject.toml".to_string(),
40 "go.mod".to_string(),
41 "pom.xml".to_string(),
42 "build.gradle".to_string(),
43 ],
44 }
45 }
46
47 pub fn add_create_file_step(mut self, path: String, content: String) -> ExecutionResult<Self> {
56 let _resolved = PathResolver::expand_home(Path::new(&path))
58 .map_err(|e| ExecutionError::ValidationError(format!("Invalid path: {}", e)))?;
59
60 let step = ExecutionStep::new(
61 format!("Create file: {}", path),
62 StepAction::CreateFile { path, content },
63 );
64
65 self.steps.push(step);
66 Ok(self)
67 }
68
69 pub fn add_modify_file_step(mut self, path: String, diff: String) -> ExecutionResult<Self> {
78 let _resolved = PathResolver::expand_home(Path::new(&path))
80 .map_err(|e| ExecutionError::ValidationError(format!("Invalid path: {}", e)))?;
81
82 let step = ExecutionStep::new(
83 format!("Modify file: {}", path),
84 StepAction::ModifyFile { path, diff },
85 );
86
87 self.steps.push(step);
88 Ok(self)
89 }
90
91 pub fn add_delete_file_step(mut self, path: String) -> ExecutionResult<Self> {
99 let _resolved = PathResolver::expand_home(Path::new(&path))
101 .map_err(|e| ExecutionError::ValidationError(format!("Invalid path: {}", e)))?;
102
103 let step = ExecutionStep::new(
104 format!("Delete file: {}", path),
105 StepAction::DeleteFile { path },
106 );
107
108 self.steps.push(step);
109 Ok(self)
110 }
111
112 pub fn add_command_step(mut self, command: String, args: Vec<String>) -> Self {
118 let step = ExecutionStep::new(
119 format!("Run command: {} {}", command, args.join(" ")),
120 StepAction::RunCommand { command, args },
121 );
122
123 self.steps.push(step);
124 self
125 }
126
127 pub fn add_test_step(mut self, pattern: Option<String>) -> Self {
132 let description = if let Some(ref p) = pattern {
133 format!("Run tests matching: {}", p)
134 } else {
135 "Run all tests".to_string()
136 };
137
138 let step = ExecutionStep::new(description, StepAction::RunTests { pattern });
139
140 self.steps.push(step);
141 self
142 }
143
144 pub fn add_dependency(mut self, step_id: String, dependency_id: String) -> Self {
150 self.dependencies
151 .entry(step_id)
152 .or_default()
153 .push(dependency_id);
154 self
155 }
156
157 pub fn with_critical_files(mut self, files: Vec<String>) -> Self {
161 self.critical_files = files;
162 self
163 }
164
165 pub fn build(mut self) -> ExecutionResult<ExecutionPlan> {
169 if self.steps.is_empty() {
170 return Err(ExecutionError::PlanError(
171 "Cannot build plan with no steps".to_string(),
172 ));
173 }
174
175 for step in &mut self.steps {
177 if let Some(deps) = self.dependencies.get(&step.id) {
178 step.dependencies = deps.clone();
179 }
180 }
181
182 let risk_score = self.calculate_risk_score();
184
185 let complexity = self.calculate_complexity();
187
188 let estimated_duration = self.estimate_duration();
190
191 let requires_approval = matches!(risk_score.level, RiskLevel::High | RiskLevel::Critical);
193
194 let plan = ExecutionPlan {
195 id: Uuid::new_v4().to_string(),
196 name: self.name,
197 steps: self.steps,
198 risk_score,
199 estimated_duration,
200 estimated_complexity: complexity,
201 requires_approval,
202 editable: true,
203 };
204
205 Ok(plan)
206 }
207
208 fn calculate_risk_score(&self) -> RiskScore {
210 let mut factors = Vec::new();
211 let mut total_score = 0.0;
212
213 let file_count = self
215 .steps
216 .iter()
217 .filter(|s| matches!(s.action, StepAction::ModifyFile { .. }))
218 .count();
219 let file_weight = file_count as f32 * 0.1;
220 factors.push(RiskFactor {
221 name: "file_count".to_string(),
222 weight: file_weight,
223 description: format!("{} files modified", file_count),
224 });
225 total_score += file_weight;
226
227 let critical_count = self
229 .steps
230 .iter()
231 .filter(|s| self.is_critical_file_step(s))
232 .count();
233 let critical_weight = critical_count as f32 * 0.5;
234 factors.push(RiskFactor {
235 name: "critical_files".to_string(),
236 weight: critical_weight,
237 description: format!("{} critical files", critical_count),
238 });
239 total_score += critical_weight;
240
241 let deletion_count = self
243 .steps
244 .iter()
245 .filter(|s| matches!(s.action, StepAction::DeleteFile { .. }))
246 .count();
247 let deletion_weight = deletion_count as f32 * 0.3;
248 factors.push(RiskFactor {
249 name: "deletions".to_string(),
250 weight: deletion_weight,
251 description: format!("{} files deleted", deletion_count),
252 });
253 total_score += deletion_weight;
254
255 let scope_weight = (self.steps.len() as f32 / 10.0).min(0.2);
257 factors.push(RiskFactor {
258 name: "scope".to_string(),
259 weight: scope_weight,
260 description: format!("{} steps", self.steps.len()),
261 });
262 total_score += scope_weight;
263
264 let level = match total_score {
265 s if s < 0.5 => RiskLevel::Low,
266 s if s < 1.5 => RiskLevel::Medium,
267 s if s < 2.5 => RiskLevel::High,
268 _ => RiskLevel::Critical,
269 };
270
271 RiskScore {
272 level,
273 score: total_score,
274 factors,
275 }
276 }
277
278 fn is_critical_file_step(&self, step: &ExecutionStep) -> bool {
280 match &step.action {
281 StepAction::CreateFile { path, .. } | StepAction::ModifyFile { path, .. } => {
282 self.critical_files.iter().any(|cf| path.ends_with(cf))
283 }
284 StepAction::DeleteFile { path } => {
285 self.critical_files.iter().any(|cf| path.ends_with(cf))
286 }
287 _ => false,
288 }
289 }
290
291 fn calculate_complexity(&self) -> ComplexityLevel {
293 let step_count = self.steps.len();
294 let has_dependencies = !self.dependencies.is_empty();
295 let has_deletions = self
296 .steps
297 .iter()
298 .any(|s| matches!(s.action, StepAction::DeleteFile { .. }));
299
300 match (step_count, has_dependencies, has_deletions) {
301 (1..=3, false, false) => ComplexityLevel::Simple,
302 (4..=8, false, false) => ComplexityLevel::Moderate,
303 (9..=15, _, false) => ComplexityLevel::Complex,
304 _ => ComplexityLevel::VeryComplex,
305 }
306 }
307
308 fn estimate_duration(&self) -> Duration {
310 let mut total_ms = 0u64;
311
312 for step in &self.steps {
313 let step_ms = match &step.action {
314 StepAction::CreateFile { content, .. } => {
315 10 + (content.len() as u64 / 100)
317 }
318 StepAction::ModifyFile { .. } => 50,
319 StepAction::DeleteFile { .. } => 10,
320 StepAction::RunCommand { .. } => 500,
321 StepAction::RunTests { .. } => 5000,
322 };
323 total_ms += step_ms;
324 }
325
326 Duration::from_millis(total_ms)
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 #[test]
335 fn test_create_builder() {
336 let builder = PlanBuilder::new("test plan".to_string());
337 assert_eq!(builder.name, "test plan");
338 assert_eq!(builder.steps.len(), 0);
339 }
340
341 #[test]
342 fn test_add_create_file_step() {
343 let builder = PlanBuilder::new("test".to_string());
344 let result = builder.add_create_file_step("test.txt".to_string(), "content".to_string());
345 assert!(result.is_ok());
346 let builder = result.unwrap();
347 assert_eq!(builder.steps.len(), 1);
348 }
349
350 #[test]
351 fn test_add_modify_file_step() {
352 let builder = PlanBuilder::new("test".to_string());
353 let result = builder.add_modify_file_step("test.txt".to_string(), "diff".to_string());
354 assert!(result.is_ok());
355 let builder = result.unwrap();
356 assert_eq!(builder.steps.len(), 1);
357 }
358
359 #[test]
360 fn test_add_delete_file_step() {
361 let builder = PlanBuilder::new("test".to_string());
362 let result = builder.add_delete_file_step("test.txt".to_string());
363 assert!(result.is_ok());
364 let builder = result.unwrap();
365 assert_eq!(builder.steps.len(), 1);
366 }
367
368 #[test]
369 fn test_add_command_step() {
370 let builder = PlanBuilder::new("test".to_string());
371 let builder = builder.add_command_step("echo".to_string(), vec!["hello".to_string()]);
372 assert_eq!(builder.steps.len(), 1);
373 }
374
375 #[test]
376 fn test_add_test_step() {
377 let builder = PlanBuilder::new("test".to_string());
378 let builder = builder.add_test_step(Some("*.rs".to_string()));
379 assert_eq!(builder.steps.len(), 1);
380 }
381
382 #[test]
383 fn test_build_simple_plan() {
384 let builder = PlanBuilder::new("test".to_string());
385 let result = builder
386 .add_create_file_step("test.txt".to_string(), "content".to_string())
387 .unwrap()
388 .build();
389
390 assert!(result.is_ok());
391 let plan = result.unwrap();
392 assert_eq!(plan.name, "test");
393 assert_eq!(plan.steps.len(), 1);
394 assert_eq!(plan.estimated_complexity, ComplexityLevel::Simple);
395 }
396
397 #[test]
398 fn test_build_empty_plan_fails() {
399 let builder = PlanBuilder::new("test".to_string());
400 let result = builder.build();
401 assert!(result.is_err());
402 }
403
404 #[test]
405 fn test_risk_score_calculation() {
406 let builder = PlanBuilder::new("test".to_string());
407 let result = builder
408 .add_create_file_step("Cargo.toml".to_string(), "content".to_string())
409 .unwrap()
410 .add_delete_file_step("old.rs".to_string())
411 .unwrap()
412 .add_delete_file_step("old2.rs".to_string())
413 .unwrap()
414 .add_delete_file_step("old3.rs".to_string())
415 .unwrap()
416 .build();
417
418 assert!(result.is_ok());
419 let plan = result.unwrap();
420 assert!(plan.risk_score.score > 0.0);
421 assert!(plan.risk_score.score > 0.5);
425 }
426
427 #[test]
428 fn test_complexity_calculation() {
429 let builder = PlanBuilder::new("simple".to_string());
430 let simple = builder
431 .add_create_file_step("a.txt".to_string(), "a".to_string())
432 .unwrap()
433 .build()
434 .unwrap();
435 assert_eq!(simple.estimated_complexity, ComplexityLevel::Simple);
436
437 let builder = PlanBuilder::new("moderate".to_string());
438 let moderate = builder
439 .add_create_file_step("a.txt".to_string(), "a".to_string())
440 .unwrap()
441 .add_create_file_step("b.txt".to_string(), "b".to_string())
442 .unwrap()
443 .add_create_file_step("c.txt".to_string(), "c".to_string())
444 .unwrap()
445 .add_create_file_step("d.txt".to_string(), "d".to_string())
446 .unwrap()
447 .build()
448 .unwrap();
449 assert_eq!(moderate.estimated_complexity, ComplexityLevel::Moderate);
450 }
451
452 #[test]
453 fn test_duration_estimation() {
454 let builder = PlanBuilder::new("test".to_string());
455 let plan = builder
456 .add_create_file_step("test.txt".to_string(), "content".to_string())
457 .unwrap()
458 .add_command_step("echo".to_string(), vec![])
459 .build()
460 .unwrap();
461
462 assert!(plan.estimated_duration.as_millis() > 0);
463 }
464
465 #[test]
466 fn test_add_dependencies() {
467 let builder = PlanBuilder::new("test".to_string());
468 let builder = builder
469 .add_create_file_step("a.txt".to_string(), "a".to_string())
470 .unwrap()
471 .add_create_file_step("b.txt".to_string(), "b".to_string())
472 .unwrap();
473
474 let step_ids: Vec<_> = builder.steps.iter().map(|s| s.id.clone()).collect();
475 let builder = builder.add_dependency(step_ids[1].clone(), step_ids[0].clone());
476
477 let plan = builder.build().unwrap();
478 assert!(!plan.steps[1].dependencies.is_empty());
479 }
480
481 #[test]
482 fn test_critical_files_detection() {
483 let builder = PlanBuilder::new("test".to_string());
484 let plan = builder
485 .add_modify_file_step("Cargo.toml".to_string(), "diff".to_string())
486 .unwrap()
487 .build()
488 .unwrap();
489
490 assert!(plan.risk_score.score > 0.0);
492 let has_critical_factor = plan
493 .risk_score
494 .factors
495 .iter()
496 .any(|f| f.name == "critical_files" && f.weight > 0.0);
497 assert!(has_critical_factor);
498 }
499}