ricecoder_specs/
workflow.rs1use crate::error::SpecError;
4use crate::models::{Spec, Task, TaskStatus};
5use std::collections::{HashMap, HashSet};
6
7#[derive(Debug, Clone)]
12pub struct WorkflowOrchestrator {
13 task_to_requirements: HashMap<String, Vec<String>>,
15 requirement_to_tasks: HashMap<String, Vec<String>>,
17 task_completion: HashMap<String, TaskStatus>,
19}
20
21impl WorkflowOrchestrator {
22 pub fn new() -> Self {
24 WorkflowOrchestrator {
25 task_to_requirements: HashMap::new(),
26 requirement_to_tasks: HashMap::new(),
27 task_completion: HashMap::new(),
28 }
29 }
30
31 pub fn link_task_to_requirements(
43 &mut self,
44 task_id: String,
45 requirement_ids: Vec<String>,
46 ) -> Result<(), SpecError> {
47 if task_id.is_empty() {
48 return Err(SpecError::InvalidFormat(
49 "Task ID cannot be empty".to_string(),
50 ));
51 }
52
53 if requirement_ids.is_empty() {
54 return Err(SpecError::InvalidFormat(
55 "At least one requirement ID must be provided".to_string(),
56 ));
57 }
58
59 self.task_to_requirements
61 .insert(task_id.clone(), requirement_ids.clone());
62
63 for req_id in requirement_ids {
65 self.requirement_to_tasks
66 .entry(req_id)
67 .or_default()
68 .push(task_id.clone());
69 }
70
71 self.task_completion
73 .entry(task_id)
74 .or_insert(TaskStatus::NotStarted);
75
76 Ok(())
77 }
78
79 pub fn get_task_requirements(&self, task_id: &str) -> Vec<String> {
87 self.task_to_requirements
88 .get(task_id)
89 .cloned()
90 .unwrap_or_default()
91 }
92
93 pub fn get_requirement_tasks(&self, requirement_id: &str) -> Vec<String> {
101 self.requirement_to_tasks
102 .get(requirement_id)
103 .cloned()
104 .unwrap_or_default()
105 }
106
107 pub fn update_task_status(
118 &mut self,
119 task_id: String,
120 status: TaskStatus,
121 ) -> Result<(), SpecError> {
122 if !self.task_completion.contains_key(&task_id) {
123 return Err(SpecError::NotFound(format!("Task not found: {}", task_id)));
124 }
125
126 self.task_completion.insert(task_id, status);
127 Ok(())
128 }
129
130 pub fn get_task_status(&self, task_id: &str) -> TaskStatus {
138 self.task_completion
139 .get(task_id)
140 .copied()
141 .unwrap_or(TaskStatus::NotStarted)
142 }
143
144 pub fn validate_task_traceability(&self, spec: &Spec) -> Result<(), SpecError> {
155 let mut unlinked_tasks = Vec::new();
156
157 let all_task_ids = self.collect_all_task_ids(&spec.tasks);
159
160 for task_id in all_task_ids {
162 if !self.task_to_requirements.contains_key(&task_id) {
163 unlinked_tasks.push(task_id);
164 }
165 }
166
167 if !unlinked_tasks.is_empty() {
168 return Err(SpecError::InvalidFormat(format!(
169 "Tasks without requirement links: {}",
170 unlinked_tasks.join(", ")
171 )));
172 }
173
174 Ok(())
175 }
176
177 pub fn validate_acceptance_criteria_coverage(&self, spec: &Spec) -> Result<(), SpecError> {
188 let mut unaddressed_criteria = Vec::new();
189
190 for requirement in &spec.requirements {
192 for criterion in &requirement.acceptance_criteria {
193 let criterion_id = format!("{}.{}", requirement.id, criterion.id);
194
195 if !self.requirement_to_tasks.contains_key(&requirement.id) {
197 unaddressed_criteria.push(criterion_id);
198 }
199 }
200 }
201
202 if !unaddressed_criteria.is_empty() {
203 return Err(SpecError::InvalidFormat(format!(
204 "Acceptance criteria without task coverage: {}",
205 unaddressed_criteria.join(", ")
206 )));
207 }
208
209 Ok(())
210 }
211
212 pub fn get_completed_tasks(&self) -> Vec<String> {
217 self.task_completion
218 .iter()
219 .filter(|(_, status)| **status == TaskStatus::Complete)
220 .map(|(id, _)| id.clone())
221 .collect()
222 }
223
224 pub fn get_in_progress_tasks(&self) -> Vec<String> {
229 self.task_completion
230 .iter()
231 .filter(|(_, status)| **status == TaskStatus::InProgress)
232 .map(|(id, _)| id.clone())
233 .collect()
234 }
235
236 pub fn get_not_started_tasks(&self) -> Vec<String> {
241 self.task_completion
242 .iter()
243 .filter(|(_, status)| **status == TaskStatus::NotStarted)
244 .map(|(id, _)| id.clone())
245 .collect()
246 }
247
248 pub fn get_completion_percentage(&self) -> f64 {
255 if self.task_completion.is_empty() {
256 return 0.0;
257 }
258
259 let completed = self
260 .task_completion
261 .values()
262 .filter(|status| **status == TaskStatus::Complete)
263 .count();
264
265 (completed as f64 / self.task_completion.len() as f64) * 100.0
266 }
267
268 #[allow(clippy::only_used_in_recursion)]
270 fn collect_all_task_ids(&self, tasks: &[Task]) -> Vec<String> {
271 let mut ids = Vec::new();
272
273 for task in tasks {
274 ids.push(task.id.clone());
275 ids.extend(self.collect_all_task_ids(&task.subtasks));
276 }
277
278 ids
279 }
280
281 pub fn get_all_linked_requirements(&self) -> HashSet<String> {
286 self.requirement_to_tasks.keys().cloned().collect()
287 }
288
289 pub fn get_all_linked_tasks(&self) -> HashSet<String> {
294 self.task_to_requirements.keys().cloned().collect()
295 }
296
297 pub fn reset(&mut self) {
299 self.task_to_requirements.clear();
300 self.requirement_to_tasks.clear();
301 self.task_completion.clear();
302 }
303}
304
305impl Default for WorkflowOrchestrator {
306 fn default() -> Self {
307 Self::new()
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
316 fn test_link_task_to_requirements() {
317 let mut orchestrator = WorkflowOrchestrator::new();
318
319 let result = orchestrator.link_task_to_requirements(
320 "task-1".to_string(),
321 vec!["REQ-1".to_string(), "REQ-2".to_string()],
322 );
323
324 assert!(result.is_ok());
325 assert_eq!(
326 orchestrator.get_task_requirements("task-1"),
327 vec!["REQ-1".to_string(), "REQ-2".to_string()]
328 );
329 }
330
331 #[test]
332 fn test_link_task_empty_task_id() {
333 let mut orchestrator = WorkflowOrchestrator::new();
334
335 let result =
336 orchestrator.link_task_to_requirements("".to_string(), vec!["REQ-1".to_string()]);
337
338 assert!(result.is_err());
339 }
340
341 #[test]
342 fn test_link_task_empty_requirements() {
343 let mut orchestrator = WorkflowOrchestrator::new();
344
345 let result = orchestrator.link_task_to_requirements("task-1".to_string(), vec![]);
346
347 assert!(result.is_err());
348 }
349
350 #[test]
351 fn test_get_requirement_tasks() {
352 let mut orchestrator = WorkflowOrchestrator::new();
353
354 orchestrator
355 .link_task_to_requirements("task-1".to_string(), vec!["REQ-1".to_string()])
356 .unwrap();
357
358 orchestrator
359 .link_task_to_requirements("task-2".to_string(), vec!["REQ-1".to_string()])
360 .unwrap();
361
362 let tasks = orchestrator.get_requirement_tasks("REQ-1");
363 assert_eq!(tasks.len(), 2);
364 assert!(tasks.contains(&"task-1".to_string()));
365 assert!(tasks.contains(&"task-2".to_string()));
366 }
367
368 #[test]
369 fn test_update_task_status() {
370 let mut orchestrator = WorkflowOrchestrator::new();
371
372 orchestrator
373 .link_task_to_requirements("task-1".to_string(), vec!["REQ-1".to_string()])
374 .unwrap();
375
376 let result = orchestrator.update_task_status("task-1".to_string(), TaskStatus::InProgress);
377 assert!(result.is_ok());
378 assert_eq!(
379 orchestrator.get_task_status("task-1"),
380 TaskStatus::InProgress
381 );
382 }
383
384 #[test]
385 fn test_update_task_status_not_found() {
386 let mut orchestrator = WorkflowOrchestrator::new();
387
388 let result =
389 orchestrator.update_task_status("nonexistent".to_string(), TaskStatus::Complete);
390 assert!(result.is_err());
391 }
392
393 #[test]
394 fn test_get_completed_tasks() {
395 let mut orchestrator = WorkflowOrchestrator::new();
396
397 orchestrator
398 .link_task_to_requirements("task-1".to_string(), vec!["REQ-1".to_string()])
399 .unwrap();
400
401 orchestrator
402 .link_task_to_requirements("task-2".to_string(), vec!["REQ-2".to_string()])
403 .unwrap();
404
405 orchestrator
406 .update_task_status("task-1".to_string(), TaskStatus::Complete)
407 .unwrap();
408
409 let completed = orchestrator.get_completed_tasks();
410 assert_eq!(completed.len(), 1);
411 assert!(completed.contains(&"task-1".to_string()));
412 }
413
414 #[test]
415 fn test_get_in_progress_tasks() {
416 let mut orchestrator = WorkflowOrchestrator::new();
417
418 orchestrator
419 .link_task_to_requirements("task-1".to_string(), vec!["REQ-1".to_string()])
420 .unwrap();
421
422 orchestrator
423 .update_task_status("task-1".to_string(), TaskStatus::InProgress)
424 .unwrap();
425
426 let in_progress = orchestrator.get_in_progress_tasks();
427 assert_eq!(in_progress.len(), 1);
428 assert!(in_progress.contains(&"task-1".to_string()));
429 }
430
431 #[test]
432 fn test_get_not_started_tasks() {
433 let mut orchestrator = WorkflowOrchestrator::new();
434
435 orchestrator
436 .link_task_to_requirements("task-1".to_string(), vec!["REQ-1".to_string()])
437 .unwrap();
438
439 let not_started = orchestrator.get_not_started_tasks();
440 assert_eq!(not_started.len(), 1);
441 assert!(not_started.contains(&"task-1".to_string()));
442 }
443
444 #[test]
445 fn test_get_completion_percentage() {
446 let mut orchestrator = WorkflowOrchestrator::new();
447
448 orchestrator
449 .link_task_to_requirements("task-1".to_string(), vec!["REQ-1".to_string()])
450 .unwrap();
451
452 orchestrator
453 .link_task_to_requirements("task-2".to_string(), vec!["REQ-2".to_string()])
454 .unwrap();
455
456 orchestrator
457 .update_task_status("task-1".to_string(), TaskStatus::Complete)
458 .unwrap();
459
460 let percentage = orchestrator.get_completion_percentage();
461 assert_eq!(percentage, 50.0);
462 }
463
464 #[test]
465 fn test_get_completion_percentage_empty() {
466 let orchestrator = WorkflowOrchestrator::new();
467 assert_eq!(orchestrator.get_completion_percentage(), 0.0);
468 }
469
470 #[test]
471 fn test_get_all_linked_requirements() {
472 let mut orchestrator = WorkflowOrchestrator::new();
473
474 orchestrator
475 .link_task_to_requirements(
476 "task-1".to_string(),
477 vec!["REQ-1".to_string(), "REQ-2".to_string()],
478 )
479 .unwrap();
480
481 let requirements = orchestrator.get_all_linked_requirements();
482 assert_eq!(requirements.len(), 2);
483 assert!(requirements.contains("REQ-1"));
484 assert!(requirements.contains("REQ-2"));
485 }
486
487 #[test]
488 fn test_get_all_linked_tasks() {
489 let mut orchestrator = WorkflowOrchestrator::new();
490
491 orchestrator
492 .link_task_to_requirements("task-1".to_string(), vec!["REQ-1".to_string()])
493 .unwrap();
494
495 orchestrator
496 .link_task_to_requirements("task-2".to_string(), vec!["REQ-2".to_string()])
497 .unwrap();
498
499 let tasks = orchestrator.get_all_linked_tasks();
500 assert_eq!(tasks.len(), 2);
501 assert!(tasks.contains("task-1"));
502 assert!(tasks.contains("task-2"));
503 }
504
505 #[test]
506 fn test_reset() {
507 let mut orchestrator = WorkflowOrchestrator::new();
508
509 orchestrator
510 .link_task_to_requirements("task-1".to_string(), vec!["REQ-1".to_string()])
511 .unwrap();
512
513 orchestrator.reset();
514
515 assert_eq!(orchestrator.get_all_linked_tasks().len(), 0);
516 assert_eq!(orchestrator.get_all_linked_requirements().len(), 0);
517 }
518}