1use crate::error::GenerationError;
7use ricecoder_specs::models::{AcceptanceCriterion, Priority, Requirement, Spec};
8use std::collections::BTreeMap;
9
10#[derive(Debug, Clone)]
12pub struct SpecProcessor;
13
14#[derive(Debug, Clone)]
16pub struct GenerationPlan {
17 pub id: String,
19 pub spec_id: String,
21 pub steps: Vec<GenerationStep>,
23 pub dependencies: Vec<(String, String)>,
25 pub constraints: Vec<Constraint>,
27}
28
29#[derive(Debug, Clone)]
31pub struct GenerationStep {
32 pub id: String,
34 pub description: String,
36 pub requirement_ids: Vec<String>,
38 pub acceptance_criteria: Vec<AcceptanceCriterion>,
40 pub priority: Priority,
42 pub optional: bool,
44 pub sequence: usize,
46}
47
48#[derive(Debug, Clone)]
50pub struct Constraint {
51 pub id: String,
53 pub description: String,
55 pub constraint_type: ConstraintType,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum ConstraintType {
62 NamingConvention,
64 CodeQuality,
66 Documentation,
68 ErrorHandling,
70 Testing,
72 Other,
74}
75
76impl SpecProcessor {
77 pub fn new() -> Self {
79 Self
80 }
81
82 pub fn process(&self, spec: &Spec) -> Result<GenerationPlan, GenerationError> {
96 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 for (_, step) in step_map {
107 steps.push(step);
108 }
109
110 steps.sort_by_key(|s| s.sequence);
112
113 let constraints = self.extract_constraints(spec)?;
115
116 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 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 fn extract_constraints(&self, spec: &Spec) -> Result<Vec<Constraint>, GenerationError> {
147 let mut constraints = Vec::new();
148
149 for requirement in &spec.requirements {
151 for criterion in &requirement.acceptance_criteria {
152 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 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 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 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 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 fn determine_dependencies(
216 &self,
217 steps: &[GenerationStep],
218 ) -> Result<Vec<(String, String)>, GenerationError> {
219 let mut dependencies = Vec::new();
220
221 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 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 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 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}