ricecoder_generation/
spec_processor.rs

1//! Spec processing for code generation
2//!
3//! Processes specifications into generation plans by extracting requirements,
4//! acceptance criteria, and constraints, then mapping them to generation tasks.
5
6use crate::error::GenerationError;
7use ricecoder_specs::models::{AcceptanceCriterion, Priority, Requirement, Spec};
8use std::collections::BTreeMap;
9
10/// Processes specifications into generation plans
11#[derive(Debug, Clone)]
12pub struct SpecProcessor;
13
14/// A generation plan derived from a specification
15#[derive(Debug, Clone)]
16pub struct GenerationPlan {
17    /// Unique identifier for the plan
18    pub id: String,
19    /// Spec ID this plan was derived from
20    pub spec_id: String,
21    /// Ordered generation steps
22    pub steps: Vec<GenerationStep>,
23    /// Dependencies between steps (step_id_a, step_id_b means a must complete before b)
24    pub dependencies: Vec<(String, String)>,
25    /// Constraints extracted from spec
26    pub constraints: Vec<Constraint>,
27}
28
29/// A single generation step
30#[derive(Debug, Clone)]
31pub struct GenerationStep {
32    /// Unique identifier for this step
33    pub id: String,
34    /// Human-readable description
35    pub description: String,
36    /// Requirements this step addresses
37    pub requirement_ids: Vec<String>,
38    /// Acceptance criteria this step must satisfy
39    pub acceptance_criteria: Vec<AcceptanceCriterion>,
40    /// Priority of this step
41    pub priority: Priority,
42    /// Whether this step is optional
43    pub optional: bool,
44    /// Order in execution sequence
45    pub sequence: usize,
46}
47
48/// A constraint extracted from requirements
49#[derive(Debug, Clone)]
50pub struct Constraint {
51    /// Constraint identifier
52    pub id: String,
53    /// Constraint description
54    pub description: String,
55    /// Type of constraint
56    pub constraint_type: ConstraintType,
57}
58
59/// Types of constraints
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum ConstraintType {
62    /// Naming convention constraint
63    NamingConvention,
64    /// Code quality constraint
65    CodeQuality,
66    /// Documentation constraint
67    Documentation,
68    /// Error handling constraint
69    ErrorHandling,
70    /// Testing constraint
71    Testing,
72    /// Other constraint
73    Other,
74}
75
76impl SpecProcessor {
77    /// Creates a new SpecProcessor
78    pub fn new() -> Self {
79        Self
80    }
81
82    /// Processes a spec into a generation plan
83    ///
84    /// # Arguments
85    ///
86    /// * `spec` - The specification to process
87    ///
88    /// # Returns
89    ///
90    /// A generation plan with ordered steps and dependencies
91    ///
92    /// # Errors
93    ///
94    /// Returns an error if the spec is invalid or cannot be processed
95    pub fn process(&self, spec: &Spec) -> Result<GenerationPlan, GenerationError> {
96        // Extract requirements and build steps
97        let mut steps = Vec::new();
98        let mut step_map: BTreeMap<String, GenerationStep> = BTreeMap::new();
99
100        for (idx, requirement) in spec.requirements.iter().enumerate() {
101            let step = self.requirement_to_step(requirement, idx)?;
102            step_map.insert(step.id.clone(), step);
103        }
104
105        // Convert to ordered vec
106        for (_, step) in step_map {
107            steps.push(step);
108        }
109
110        // Sort by sequence
111        steps.sort_by_key(|s| s.sequence);
112
113        // Extract constraints
114        let constraints = self.extract_constraints(spec)?;
115
116        // Determine dependencies based on requirement relationships
117        let dependencies = self.determine_dependencies(&steps)?;
118
119        Ok(GenerationPlan {
120            id: format!("plan-{}", uuid::Uuid::new_v4()),
121            spec_id: spec.id.clone(),
122            steps,
123            dependencies,
124            constraints,
125        })
126    }
127
128    /// Converts a requirement to a generation step
129    fn requirement_to_step(
130        &self,
131        requirement: &Requirement,
132        sequence: usize,
133    ) -> Result<GenerationStep, GenerationError> {
134        Ok(GenerationStep {
135            id: format!("step-{}", requirement.id),
136            description: requirement.user_story.clone(),
137            requirement_ids: vec![requirement.id.clone()],
138            acceptance_criteria: requirement.acceptance_criteria.clone(),
139            priority: requirement.priority,
140            optional: false,
141            sequence,
142        })
143    }
144
145    /// Extracts constraints from the specification
146    fn extract_constraints(&self, spec: &Spec) -> Result<Vec<Constraint>, GenerationError> {
147        let mut constraints = Vec::new();
148
149        // Extract constraints from requirements
150        for requirement in &spec.requirements {
151            for criterion in &requirement.acceptance_criteria {
152                // Look for naming convention constraints
153                if criterion.then.to_lowercase().contains("naming convention")
154                    || criterion.then.to_lowercase().contains("snake_case")
155                    || criterion.then.to_lowercase().contains("camelcase")
156                    || criterion.then.to_lowercase().contains("pascalcase")
157                {
158                    constraints.push(Constraint {
159                        id: format!("constraint-naming-{}", criterion.id),
160                        description: criterion.then.clone(),
161                        constraint_type: ConstraintType::NamingConvention,
162                    });
163                }
164
165                // Look for documentation constraints
166                if criterion.then.to_lowercase().contains("doc comment")
167                    || criterion.then.to_lowercase().contains("documentation")
168                {
169                    constraints.push(Constraint {
170                        id: format!("constraint-doc-{}", criterion.id),
171                        description: criterion.then.clone(),
172                        constraint_type: ConstraintType::Documentation,
173                    });
174                }
175
176                // Look for error handling constraints
177                if criterion.then.to_lowercase().contains("error handling")
178                    || criterion.then.to_lowercase().contains("error type")
179                {
180                    constraints.push(Constraint {
181                        id: format!("constraint-error-{}", criterion.id),
182                        description: criterion.then.clone(),
183                        constraint_type: ConstraintType::ErrorHandling,
184                    });
185                }
186
187                // Look for testing constraints
188                if criterion.then.to_lowercase().contains("test")
189                    || criterion.then.to_lowercase().contains("unit test")
190                {
191                    constraints.push(Constraint {
192                        id: format!("constraint-test-{}", criterion.id),
193                        description: criterion.then.clone(),
194                        constraint_type: ConstraintType::Testing,
195                    });
196                }
197
198                // Look for code quality constraints
199                if criterion.then.to_lowercase().contains("quality")
200                    || criterion.then.to_lowercase().contains("standard")
201                {
202                    constraints.push(Constraint {
203                        id: format!("constraint-quality-{}", criterion.id),
204                        description: criterion.then.clone(),
205                        constraint_type: ConstraintType::CodeQuality,
206                    });
207                }
208            }
209        }
210
211        Ok(constraints)
212    }
213
214    /// Determines dependencies between generation steps
215    fn determine_dependencies(
216        &self,
217        steps: &[GenerationStep],
218    ) -> Result<Vec<(String, String)>, GenerationError> {
219        let mut dependencies = Vec::new();
220
221        // For now, steps are executed in sequence
222        // In the future, this could analyze requirement relationships
223        for i in 0..steps.len().saturating_sub(1) {
224            dependencies.push((steps[i].id.clone(), steps[i + 1].id.clone()));
225        }
226
227        Ok(dependencies)
228    }
229}
230
231impl Default for SpecProcessor {
232    fn default() -> Self {
233        Self::new()
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use chrono::Utc;
241    use ricecoder_specs::models::{SpecMetadata, SpecPhase, SpecStatus};
242
243    fn create_test_spec() -> Spec {
244        Spec {
245            id: "test-spec".to_string(),
246            name: "Test Spec".to_string(),
247            version: "1.0.0".to_string(),
248            requirements: vec![
249                Requirement {
250                    id: "req-1".to_string(),
251                    user_story: "As a user, I want feature X".to_string(),
252                    acceptance_criteria: vec![
253                        AcceptanceCriterion {
254                            id: "ac-1-1".to_string(),
255                            when: "I do action A".to_string(),
256                            then: "The system SHALL use snake_case naming convention".to_string(),
257                        },
258                        AcceptanceCriterion {
259                            id: "ac-1-2".to_string(),
260                            when: "I do action B".to_string(),
261                            then: "The system SHALL include doc comments for all public functions"
262                                .to_string(),
263                        },
264                    ],
265                    priority: Priority::Must,
266                },
267                Requirement {
268                    id: "req-2".to_string(),
269                    user_story: "As a user, I want feature Y".to_string(),
270                    acceptance_criteria: vec![AcceptanceCriterion {
271                        id: "ac-2-1".to_string(),
272                        when: "I do action C".to_string(),
273                        then: "The system SHALL include error handling".to_string(),
274                    }],
275                    priority: Priority::Should,
276                },
277            ],
278            design: None,
279            tasks: vec![],
280            metadata: SpecMetadata {
281                author: Some("Test Author".to_string()),
282                created_at: Utc::now(),
283                updated_at: Utc::now(),
284                phase: SpecPhase::Requirements,
285                status: SpecStatus::Approved,
286            },
287            inheritance: None,
288        }
289    }
290
291    #[test]
292    fn test_process_creates_plan_with_steps() {
293        let processor = SpecProcessor::new();
294        let spec = create_test_spec();
295
296        let plan = processor.process(&spec).expect("Failed to process spec");
297
298        assert_eq!(plan.spec_id, "test-spec");
299        assert_eq!(plan.steps.len(), 2);
300        assert_eq!(plan.steps[0].requirement_ids, vec!["req-1"]);
301        assert_eq!(plan.steps[1].requirement_ids, vec!["req-2"]);
302    }
303
304    #[test]
305    fn test_process_extracts_constraints() {
306        let processor = SpecProcessor::new();
307        let spec = create_test_spec();
308
309        let plan = processor.process(&spec).expect("Failed to process spec");
310
311        // Should extract naming, documentation, and error handling constraints
312        assert!(!plan.constraints.is_empty());
313
314        let has_naming = plan
315            .constraints
316            .iter()
317            .any(|c| c.constraint_type == ConstraintType::NamingConvention);
318        let has_doc = plan
319            .constraints
320            .iter()
321            .any(|c| c.constraint_type == ConstraintType::Documentation);
322        let has_error = plan
323            .constraints
324            .iter()
325            .any(|c| c.constraint_type == ConstraintType::ErrorHandling);
326
327        assert!(has_naming, "Should extract naming constraint");
328        assert!(has_doc, "Should extract documentation constraint");
329        assert!(has_error, "Should extract error handling constraint");
330    }
331
332    #[test]
333    fn test_process_determines_dependencies() {
334        let processor = SpecProcessor::new();
335        let spec = create_test_spec();
336
337        let plan = processor.process(&spec).expect("Failed to process spec");
338
339        // With 2 steps, should have 1 dependency
340        assert_eq!(plan.dependencies.len(), 1);
341        assert_eq!(plan.dependencies[0].0, plan.steps[0].id);
342        assert_eq!(plan.dependencies[0].1, plan.steps[1].id);
343    }
344
345    #[test]
346    fn test_requirement_to_step_preserves_priority() {
347        let processor = SpecProcessor::new();
348        let requirement = Requirement {
349            id: "req-test".to_string(),
350            user_story: "Test story".to_string(),
351            acceptance_criteria: vec![],
352            priority: Priority::Must,
353        };
354
355        let step = processor
356            .requirement_to_step(&requirement, 0)
357            .expect("Failed to convert requirement");
358
359        assert_eq!(step.priority, Priority::Must);
360        assert_eq!(step.sequence, 0);
361    }
362
363    #[test]
364    fn test_extract_constraints_identifies_all_types() {
365        let processor = SpecProcessor::new();
366        let spec = create_test_spec();
367
368        let constraints = processor
369            .extract_constraints(&spec)
370            .expect("Failed to extract constraints");
371
372        // Verify we have constraints of different types
373        let types: Vec<_> = constraints.iter().map(|c| c.constraint_type).collect();
374        assert!(types.contains(&ConstraintType::NamingConvention));
375        assert!(types.contains(&ConstraintType::Documentation));
376        assert!(types.contains(&ConstraintType::ErrorHandling));
377    }
378}