1use crate::error::GenerationError;
7use crate::spec_processor::{ConstraintType, GenerationPlan, GenerationStep};
8use ricecoder_specs::models::{Priority, Requirement};
9use std::collections::{HashMap, HashSet};
10
11#[derive(Debug, Clone)]
13pub struct GenerationPlanBuilder {
14 max_steps: usize,
16}
17
18#[derive(Debug, Clone)]
20pub struct PlanValidation {
21 pub is_valid: bool,
23 pub errors: Vec<String>,
25 pub warnings: Vec<String>,
27}
28
29impl GenerationPlanBuilder {
30 pub fn new() -> Self {
32 Self { max_steps: 1000 }
33 }
34
35 pub fn with_max_steps(mut self, max_steps: usize) -> Self {
37 self.max_steps = max_steps;
38 self
39 }
40
41 pub fn create_steps(
51 &self,
52 requirements: &[Requirement],
53 ) -> Result<Vec<GenerationStep>, GenerationError> {
54 if requirements.len() > self.max_steps {
55 return Err(GenerationError::SpecError(format!(
56 "Too many requirements: {} exceeds maximum of {}",
57 requirements.len(),
58 self.max_steps
59 )));
60 }
61
62 let mut steps = Vec::new();
63
64 for (idx, requirement) in requirements.iter().enumerate() {
65 let step = GenerationStep {
66 id: format!("step-{}", requirement.id),
67 description: requirement.user_story.clone(),
68 requirement_ids: vec![requirement.id.clone()],
69 acceptance_criteria: requirement.acceptance_criteria.clone(),
70 priority: requirement.priority,
71 optional: requirement.priority == Priority::Could,
72 sequence: idx,
73 };
74 steps.push(step);
75 }
76
77 Ok(steps)
78 }
79
80 pub fn determine_dependencies(
90 &self,
91 steps: &[GenerationStep],
92 ) -> Result<Vec<(String, String)>, GenerationError> {
93 let mut dependencies = Vec::new();
94
95 let mut step_sequence: HashMap<String, usize> = HashMap::new();
97 for step in steps {
98 step_sequence.insert(step.id.clone(), step.sequence);
99 }
100
101 for i in 0..steps.len().saturating_sub(1) {
104 let current_step = &steps[i];
105 let next_step = &steps[i + 1];
106
107 if next_step.sequence > current_step.sequence {
109 dependencies.push((current_step.id.clone(), next_step.id.clone()));
110 }
111 }
112
113 Ok(dependencies)
114 }
115
116 pub fn order_steps(
127 &self,
128 mut steps: Vec<GenerationStep>,
129 dependencies: &[(String, String)],
130 ) -> Result<Vec<GenerationStep>, GenerationError> {
131 let mut dep_graph: HashMap<String, Vec<String>> = HashMap::new();
133 let mut in_degree: HashMap<String, usize> = HashMap::new();
134
135 for step in &steps {
137 dep_graph.insert(step.id.clone(), Vec::new());
138 in_degree.insert(step.id.clone(), 0);
139 }
140
141 for (from, to) in dependencies {
143 if let Some(deps) = dep_graph.get_mut(from) {
144 deps.push(to.clone());
145 }
146 *in_degree.get_mut(to).unwrap_or(&mut 0) += 1;
147 }
148
149 let mut queue: Vec<String> = in_degree
151 .iter()
152 .filter(|(_, °ree)| degree == 0)
153 .map(|(id, _)| id.clone())
154 .collect();
155
156 let mut ordered = Vec::new();
157
158 while !queue.is_empty() {
159 queue.sort(); let current = queue.remove(0);
161
162 if let Some(pos) = steps.iter().position(|s| s.id == current) {
164 ordered.push(steps.remove(pos));
165 }
166
167 if let Some(neighbors) = dep_graph.get(¤t) {
169 for neighbor in neighbors.clone() {
170 let degree = in_degree.get_mut(&neighbor).unwrap();
171 *degree -= 1;
172 if *degree == 0 {
173 queue.push(neighbor);
174 }
175 }
176 }
177 }
178
179 if !steps.is_empty() {
180 return Err(GenerationError::SpecError(
181 "Circular dependency detected in generation steps".to_string(),
182 ));
183 }
184
185 Ok(ordered)
186 }
187
188 pub fn validate_plan(&self, plan: &GenerationPlan) -> PlanValidation {
198 let mut errors = Vec::new();
199 let mut warnings = Vec::new();
200
201 if plan.steps.is_empty() {
203 errors.push("Generation plan has no steps".to_string());
204 }
205
206 let mut seen_ids = HashSet::new();
208 for step in &plan.steps {
209 if !seen_ids.insert(&step.id) {
210 errors.push(format!("Duplicate step ID: {}", step.id));
211 }
212 }
213
214 let step_ids: HashSet<_> = plan.steps.iter().map(|s| &s.id).collect();
216 for (from, to) in &plan.dependencies {
217 if !step_ids.contains(from) {
218 errors.push(format!("Dependency references non-existent step: {}", from));
219 }
220 if !step_ids.contains(to) {
221 errors.push(format!("Dependency references non-existent step: {}", to));
222 }
223 }
224
225 for step in &plan.steps {
227 if step.acceptance_criteria.is_empty() && !step.optional {
228 warnings.push(format!("Step {} has no acceptance criteria", step.id));
229 }
230 }
231
232 let has_quality_constraints = plan
234 .constraints
235 .iter()
236 .any(|c| c.constraint_type == ConstraintType::CodeQuality);
237
238 if !plan.steps.is_empty() && !has_quality_constraints {
239 warnings.push("No code quality constraints found in plan".to_string());
240 }
241
242 let is_valid = errors.is_empty();
243
244 PlanValidation {
245 is_valid,
246 errors,
247 warnings,
248 }
249 }
250}
251
252impl Default for GenerationPlanBuilder {
253 fn default() -> Self {
254 Self::new()
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use ricecoder_specs::models::AcceptanceCriterion;
262
263 fn create_test_requirement(id: &str, priority: Priority) -> Requirement {
264 Requirement {
265 id: id.to_string(),
266 user_story: format!("User story for {}", id),
267 acceptance_criteria: vec![AcceptanceCriterion {
268 id: format!("{}-ac-1", id),
269 when: "condition".to_string(),
270 then: "outcome".to_string(),
271 }],
272 priority,
273 }
274 }
275
276 #[test]
277 fn test_create_steps_from_requirements() {
278 let builder = GenerationPlanBuilder::new();
279 let requirements = vec![
280 create_test_requirement("req-1", Priority::Must),
281 create_test_requirement("req-2", Priority::Should),
282 ];
283
284 let steps = builder
285 .create_steps(&requirements)
286 .expect("Failed to create steps");
287
288 assert_eq!(steps.len(), 2);
289 assert_eq!(steps[0].id, "step-req-1");
290 assert_eq!(steps[1].id, "step-req-2");
291 assert_eq!(steps[0].sequence, 0);
292 assert_eq!(steps[1].sequence, 1);
293 }
294
295 #[test]
296 fn test_create_steps_marks_optional() {
297 let builder = GenerationPlanBuilder::new();
298 let requirements = vec![
299 create_test_requirement("req-1", Priority::Must),
300 create_test_requirement("req-2", Priority::Could),
301 ];
302
303 let steps = builder
304 .create_steps(&requirements)
305 .expect("Failed to create steps");
306
307 assert!(!steps[0].optional);
308 assert!(steps[1].optional);
309 }
310
311 #[test]
312 fn test_determine_dependencies_sequential() {
313 let builder = GenerationPlanBuilder::new();
314 let steps = vec![
315 GenerationStep {
316 id: "step-1".to_string(),
317 description: "Step 1".to_string(),
318 requirement_ids: vec!["req-1".to_string()],
319 acceptance_criteria: vec![],
320 priority: Priority::Must,
321 optional: false,
322 sequence: 0,
323 },
324 GenerationStep {
325 id: "step-2".to_string(),
326 description: "Step 2".to_string(),
327 requirement_ids: vec!["req-2".to_string()],
328 acceptance_criteria: vec![],
329 priority: Priority::Must,
330 optional: false,
331 sequence: 1,
332 },
333 ];
334
335 let deps = builder
336 .determine_dependencies(&steps)
337 .expect("Failed to determine dependencies");
338
339 assert_eq!(deps.len(), 1);
340 assert_eq!(deps[0], ("step-1".to_string(), "step-2".to_string()));
341 }
342
343 #[test]
344 fn test_order_steps_topological_sort() {
345 let builder = GenerationPlanBuilder::new();
346 let steps = vec![
347 GenerationStep {
348 id: "step-1".to_string(),
349 description: "Step 1".to_string(),
350 requirement_ids: vec![],
351 acceptance_criteria: vec![],
352 priority: Priority::Must,
353 optional: false,
354 sequence: 0,
355 },
356 GenerationStep {
357 id: "step-2".to_string(),
358 description: "Step 2".to_string(),
359 requirement_ids: vec![],
360 acceptance_criteria: vec![],
361 priority: Priority::Must,
362 optional: false,
363 sequence: 1,
364 },
365 ];
366
367 let dependencies = vec![("step-1".to_string(), "step-2".to_string())];
368
369 let ordered = builder
370 .order_steps(steps, &dependencies)
371 .expect("Failed to order steps");
372
373 assert_eq!(ordered[0].id, "step-1");
374 assert_eq!(ordered[1].id, "step-2");
375 }
376
377 #[test]
378 fn test_validate_plan_empty_steps() {
379 let builder = GenerationPlanBuilder::new();
380 let plan = GenerationPlan {
381 id: "plan-1".to_string(),
382 spec_id: "spec-1".to_string(),
383 steps: vec![],
384 dependencies: vec![],
385 constraints: vec![],
386 };
387
388 let validation = builder.validate_plan(&plan);
389
390 assert!(!validation.is_valid);
391 assert!(validation.errors.iter().any(|e| e.contains("no steps")));
392 }
393
394 #[test]
395 fn test_validate_plan_duplicate_ids() {
396 let builder = GenerationPlanBuilder::new();
397 let plan = GenerationPlan {
398 id: "plan-1".to_string(),
399 spec_id: "spec-1".to_string(),
400 steps: vec![
401 GenerationStep {
402 id: "step-1".to_string(),
403 description: "Step 1".to_string(),
404 requirement_ids: vec![],
405 acceptance_criteria: vec![],
406 priority: Priority::Must,
407 optional: false,
408 sequence: 0,
409 },
410 GenerationStep {
411 id: "step-1".to_string(),
412 description: "Step 1 duplicate".to_string(),
413 requirement_ids: vec![],
414 acceptance_criteria: vec![],
415 priority: Priority::Must,
416 optional: false,
417 sequence: 1,
418 },
419 ],
420 dependencies: vec![],
421 constraints: vec![],
422 };
423
424 let validation = builder.validate_plan(&plan);
425
426 assert!(!validation.is_valid);
427 assert!(validation.errors.iter().any(|e| e.contains("Duplicate")));
428 }
429
430 #[test]
431 fn test_validate_plan_invalid_dependencies() {
432 let builder = GenerationPlanBuilder::new();
433 let plan = GenerationPlan {
434 id: "plan-1".to_string(),
435 spec_id: "spec-1".to_string(),
436 steps: vec![GenerationStep {
437 id: "step-1".to_string(),
438 description: "Step 1".to_string(),
439 requirement_ids: vec![],
440 acceptance_criteria: vec![],
441 priority: Priority::Must,
442 optional: false,
443 sequence: 0,
444 }],
445 dependencies: vec![("step-1".to_string(), "step-nonexistent".to_string())],
446 constraints: vec![],
447 };
448
449 let validation = builder.validate_plan(&plan);
450
451 assert!(!validation.is_valid);
452 assert!(validation.errors.iter().any(|e| e.contains("non-existent")));
453 }
454}