ricecoder_specs/
query.rs

1//! Query engine for spec discovery and filtering
2
3use crate::models::{Priority, Spec, SpecQuery, SpecType};
4use regex::Regex;
5
6/// Enables efficient spec discovery and filtering
7pub struct SpecQueryEngine;
8
9impl SpecQueryEngine {
10    /// Execute a query against specs
11    ///
12    /// Filters specs by name (exact or partial match), type, status, priority, phase,
13    /// and custom criteria. Returns all specs matching all filter criteria.
14    ///
15    /// # Arguments
16    ///
17    /// * `specs` - Slice of specs to query
18    /// * `query` - Query with filter criteria
19    ///
20    /// # Returns
21    ///
22    /// Vector of specs matching all filter criteria
23    pub fn query(specs: &[Spec], query: &SpecQuery) -> Vec<Spec> {
24        specs
25            .iter()
26            .filter(|spec| Self::matches_query(spec, query))
27            .cloned()
28            .collect()
29    }
30
31    /// Check if a spec matches all query criteria
32    fn matches_query(spec: &Spec, query: &SpecQuery) -> bool {
33        // Check name filter (exact or partial match)
34        if let Some(ref name_filter) = query.name {
35            if !Self::matches_name(spec, name_filter) {
36                return false;
37            }
38        }
39
40        // Check type filter
41        if let Some(spec_type) = query.spec_type {
42            if !Self::matches_type(spec, spec_type) {
43                return false;
44            }
45        }
46
47        // Check status filter
48        if let Some(status) = query.status {
49            if spec.metadata.status != status {
50                return false;
51            }
52        }
53
54        // Check priority filter
55        if let Some(priority) = query.priority {
56            if !Self::matches_priority(spec, priority) {
57                return false;
58            }
59        }
60
61        // Check phase filter
62        if let Some(phase) = query.phase {
63            if spec.metadata.phase != phase {
64                return false;
65            }
66        }
67
68        // Check custom filters
69        for (field, value) in &query.custom_filters {
70            if !Self::matches_custom_filter(spec, field, value) {
71                return false;
72            }
73        }
74
75        true
76    }
77
78    /// Check if spec name matches filter (exact or partial match)
79    fn matches_name(spec: &Spec, filter: &str) -> bool {
80        // Try exact match first
81        if spec.name.eq_ignore_ascii_case(filter) {
82            return true;
83        }
84
85        // Try partial match (case-insensitive)
86        if spec.name.to_lowercase().contains(&filter.to_lowercase()) {
87            return true;
88        }
89
90        // Try ID match
91        if spec.id.eq_ignore_ascii_case(filter) {
92            return true;
93        }
94
95        // Try regex match
96        if let Ok(regex) = Regex::new(filter) {
97            if regex.is_match(&spec.name) || regex.is_match(&spec.id) {
98                return true;
99            }
100        }
101
102        false
103    }
104
105    /// Check if spec type matches filter
106    fn matches_type(spec: &Spec, spec_type: SpecType) -> bool {
107        // Infer type from spec structure
108        let inferred_type = if !spec.requirements.is_empty() {
109            SpecType::Feature
110        } else if spec.design.is_some() {
111            SpecType::Component
112        } else if !spec.tasks.is_empty() {
113            SpecType::Task
114        } else {
115            SpecType::Feature // Default
116        };
117
118        inferred_type == spec_type
119    }
120
121    /// Check if spec has any requirement with matching priority
122    fn matches_priority(spec: &Spec, priority: Priority) -> bool {
123        spec.requirements.iter().any(|req| req.priority == priority)
124    }
125
126    /// Check if spec matches custom filter
127    fn matches_custom_filter(spec: &Spec, field: &str, value: &str) -> bool {
128        match field.to_lowercase().as_str() {
129            "author" => spec
130                .metadata
131                .author
132                .as_ref()
133                .map(|a| a.contains(value))
134                .unwrap_or(false),
135            "version" => spec.version.contains(value),
136            "id" => spec.id.contains(value),
137            _ => false,
138        }
139    }
140
141    /// Resolve dependencies for a spec
142    ///
143    /// Returns IDs of all specs that this spec depends on (via requirement references).
144    ///
145    /// # Arguments
146    ///
147    /// * `spec` - Spec to resolve dependencies for
148    /// * `all_specs` - All available specs
149    ///
150    /// # Returns
151    ///
152    /// Vector of spec IDs that this spec depends on
153    pub fn resolve_dependencies(spec: &Spec, all_specs: &[Spec]) -> Vec<String> {
154        let mut dependencies = Vec::new();
155
156        // Collect all requirement IDs referenced by tasks
157        let mut referenced_req_ids = std::collections::HashSet::new();
158        for task in &spec.tasks {
159            Self::collect_requirement_ids(task, &mut referenced_req_ids);
160        }
161
162        // Find specs that provide these requirements
163        for other_spec in all_specs {
164            if other_spec.id == spec.id {
165                continue;
166            }
167
168            for req in &other_spec.requirements {
169                if referenced_req_ids.contains(&req.id) {
170                    dependencies.push(other_spec.id.clone());
171                    break;
172                }
173            }
174        }
175
176        dependencies.sort();
177        dependencies.dedup();
178        dependencies
179    }
180
181    /// Collect all requirement IDs from a task and its subtasks
182    fn collect_requirement_ids(
183        task: &crate::models::Task,
184        ids: &mut std::collections::HashSet<String>,
185    ) {
186        for req_id in &task.requirements {
187            ids.insert(req_id.clone());
188        }
189        for subtask in &task.subtasks {
190            Self::collect_requirement_ids(subtask, ids);
191        }
192    }
193
194    /// Detect circular dependencies
195    ///
196    /// Returns a vector of cycles, where each cycle is a vector of spec IDs
197    /// forming a circular dependency chain.
198    ///
199    /// # Arguments
200    ///
201    /// * `specs` - All specs to check
202    ///
203    /// # Returns
204    ///
205    /// Vector of cycles (each cycle is a vector of spec IDs)
206    pub fn detect_circular_dependencies(specs: &[Spec]) -> Vec<Vec<String>> {
207        let mut cycles = Vec::new();
208        let mut visited = std::collections::HashSet::new();
209        let mut rec_stack = std::collections::HashSet::new();
210
211        for spec in specs {
212            if !visited.contains(&spec.id) {
213                Self::dfs_detect_cycle(
214                    &spec.id,
215                    specs,
216                    &mut visited,
217                    &mut rec_stack,
218                    &mut Vec::new(),
219                    &mut cycles,
220                );
221            }
222        }
223
224        cycles
225    }
226
227    /// Depth-first search to detect cycles
228    fn dfs_detect_cycle(
229        spec_id: &str,
230        all_specs: &[Spec],
231        visited: &mut std::collections::HashSet<String>,
232        rec_stack: &mut std::collections::HashSet<String>,
233        path: &mut Vec<String>,
234        cycles: &mut Vec<Vec<String>>,
235    ) {
236        visited.insert(spec_id.to_string());
237        rec_stack.insert(spec_id.to_string());
238        path.push(spec_id.to_string());
239
240        // Find the spec
241        if let Some(spec) = all_specs.iter().find(|s| s.id == spec_id) {
242            let dependencies = Self::resolve_dependencies(spec, all_specs);
243
244            for dep_id in dependencies {
245                if !visited.contains(&dep_id) {
246                    Self::dfs_detect_cycle(&dep_id, all_specs, visited, rec_stack, path, cycles);
247                } else if rec_stack.contains(&dep_id) {
248                    // Found a cycle
249                    if let Some(pos) = path.iter().position(|id| id == &dep_id) {
250                        let cycle = path[pos..].to_vec();
251                        cycles.push(cycle);
252                    }
253                }
254            }
255        }
256
257        rec_stack.remove(spec_id);
258        path.pop();
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265    use crate::models::{SpecMetadata, SpecPhase, SpecStatus};
266    use chrono::Utc;
267
268    fn create_test_spec(id: &str, name: &str, status: SpecStatus, phase: SpecPhase) -> Spec {
269        Spec {
270            id: id.to_string(),
271            name: name.to_string(),
272            version: "1.0.0".to_string(),
273            requirements: vec![],
274            design: None,
275            tasks: vec![],
276            metadata: SpecMetadata {
277                author: Some("Test Author".to_string()),
278                created_at: Utc::now(),
279                updated_at: Utc::now(),
280                phase,
281                status,
282            },
283            inheritance: None,
284        }
285    }
286
287    // ============================================================================
288    // Query Tests
289    // ============================================================================
290
291    #[test]
292    fn test_query_empty_specs() {
293        let specs = vec![];
294        let query = SpecQuery::default();
295        let results = SpecQueryEngine::query(&specs, &query);
296        assert_eq!(results.len(), 0);
297    }
298
299    #[test]
300    fn test_query_no_filters() {
301        let specs = vec![
302            create_test_spec(
303                "spec-1",
304                "Feature One",
305                SpecStatus::Draft,
306                SpecPhase::Requirements,
307            ),
308            create_test_spec(
309                "spec-2",
310                "Feature Two",
311                SpecStatus::Approved,
312                SpecPhase::Design,
313            ),
314        ];
315        let query = SpecQuery::default();
316        let results = SpecQueryEngine::query(&specs, &query);
317        assert_eq!(results.len(), 2);
318    }
319
320    #[test]
321    fn test_query_by_name_exact_match() {
322        let specs = vec![
323            create_test_spec(
324                "spec-1",
325                "Feature One",
326                SpecStatus::Draft,
327                SpecPhase::Requirements,
328            ),
329            create_test_spec(
330                "spec-2",
331                "Feature Two",
332                SpecStatus::Approved,
333                SpecPhase::Design,
334            ),
335        ];
336        let query = SpecQuery {
337            name: Some("Feature One".to_string()),
338            ..Default::default()
339        };
340        let results = SpecQueryEngine::query(&specs, &query);
341        assert_eq!(results.len(), 1);
342        assert_eq!(results[0].id, "spec-1");
343    }
344
345    #[test]
346    fn test_query_by_name_partial_match() {
347        let specs = vec![
348            create_test_spec(
349                "spec-1",
350                "Feature One",
351                SpecStatus::Draft,
352                SpecPhase::Requirements,
353            ),
354            create_test_spec(
355                "spec-2",
356                "Feature Two",
357                SpecStatus::Approved,
358                SpecPhase::Design,
359            ),
360        ];
361        let query = SpecQuery {
362            name: Some("Feature".to_string()),
363            ..Default::default()
364        };
365        let results = SpecQueryEngine::query(&specs, &query);
366        assert_eq!(results.len(), 2);
367    }
368
369    #[test]
370    fn test_query_by_name_case_insensitive() {
371        let specs = vec![create_test_spec(
372            "spec-1",
373            "Feature One",
374            SpecStatus::Draft,
375            SpecPhase::Requirements,
376        )];
377        let query = SpecQuery {
378            name: Some("feature one".to_string()),
379            ..Default::default()
380        };
381        let results = SpecQueryEngine::query(&specs, &query);
382        assert_eq!(results.len(), 1);
383    }
384
385    #[test]
386    fn test_query_by_status() {
387        let specs = vec![
388            create_test_spec(
389                "spec-1",
390                "Feature One",
391                SpecStatus::Draft,
392                SpecPhase::Requirements,
393            ),
394            create_test_spec(
395                "spec-2",
396                "Feature Two",
397                SpecStatus::Approved,
398                SpecPhase::Design,
399            ),
400        ];
401        let query = SpecQuery {
402            status: Some(SpecStatus::Approved),
403            ..Default::default()
404        };
405        let results = SpecQueryEngine::query(&specs, &query);
406        assert_eq!(results.len(), 1);
407        assert_eq!(results[0].id, "spec-2");
408    }
409
410    #[test]
411    fn test_query_by_phase() {
412        let specs = vec![
413            create_test_spec(
414                "spec-1",
415                "Feature One",
416                SpecStatus::Draft,
417                SpecPhase::Requirements,
418            ),
419            create_test_spec(
420                "spec-2",
421                "Feature Two",
422                SpecStatus::Approved,
423                SpecPhase::Design,
424            ),
425        ];
426        let query = SpecQuery {
427            phase: Some(SpecPhase::Design),
428            ..Default::default()
429        };
430        let results = SpecQueryEngine::query(&specs, &query);
431        assert_eq!(results.len(), 1);
432        assert_eq!(results[0].id, "spec-2");
433    }
434
435    #[test]
436    fn test_query_by_multiple_filters() {
437        let specs = vec![
438            create_test_spec(
439                "spec-1",
440                "Feature One",
441                SpecStatus::Draft,
442                SpecPhase::Requirements,
443            ),
444            create_test_spec(
445                "spec-2",
446                "Feature Two",
447                SpecStatus::Approved,
448                SpecPhase::Design,
449            ),
450            create_test_spec(
451                "spec-3",
452                "Feature Three",
453                SpecStatus::Approved,
454                SpecPhase::Requirements,
455            ),
456        ];
457        let query = SpecQuery {
458            status: Some(SpecStatus::Approved),
459            phase: Some(SpecPhase::Design),
460            ..Default::default()
461        };
462        let results = SpecQueryEngine::query(&specs, &query);
463        assert_eq!(results.len(), 1);
464        assert_eq!(results[0].id, "spec-2");
465    }
466
467    #[test]
468    fn test_query_by_custom_filter_author() {
469        let mut spec1 = create_test_spec(
470            "spec-1",
471            "Feature One",
472            SpecStatus::Draft,
473            SpecPhase::Requirements,
474        );
475        spec1.metadata.author = Some("Alice".to_string());
476
477        let mut spec2 = create_test_spec(
478            "spec-2",
479            "Feature Two",
480            SpecStatus::Approved,
481            SpecPhase::Design,
482        );
483        spec2.metadata.author = Some("Bob".to_string());
484
485        let specs = vec![spec1, spec2];
486        let query = SpecQuery {
487            custom_filters: vec![("author".to_string(), "Alice".to_string())],
488            ..Default::default()
489        };
490        let results = SpecQueryEngine::query(&specs, &query);
491        assert_eq!(results.len(), 1);
492        assert_eq!(results[0].id, "spec-1");
493    }
494
495    #[test]
496    fn test_query_by_custom_filter_version() {
497        let mut spec1 = create_test_spec(
498            "spec-1",
499            "Feature One",
500            SpecStatus::Draft,
501            SpecPhase::Requirements,
502        );
503        spec1.version = "1.0.0".to_string();
504
505        let mut spec2 = create_test_spec(
506            "spec-2",
507            "Feature Two",
508            SpecStatus::Approved,
509            SpecPhase::Design,
510        );
511        spec2.version = "2.0.0".to_string();
512
513        let specs = vec![spec1, spec2];
514        let query = SpecQuery {
515            custom_filters: vec![("version".to_string(), "2.0".to_string())],
516            ..Default::default()
517        };
518        let results = SpecQueryEngine::query(&specs, &query);
519        assert_eq!(results.len(), 1);
520        assert_eq!(results[0].id, "spec-2");
521    }
522
523    // ============================================================================
524    // Dependency Resolution Tests
525    // ============================================================================
526
527    #[test]
528    fn test_resolve_dependencies_no_dependencies() {
529        let spec = create_test_spec(
530            "spec-1",
531            "Feature One",
532            SpecStatus::Draft,
533            SpecPhase::Requirements,
534        );
535        let all_specs = vec![spec.clone()];
536        let deps = SpecQueryEngine::resolve_dependencies(&spec, &all_specs);
537        assert_eq!(deps.len(), 0);
538    }
539
540    #[test]
541    fn test_resolve_dependencies_with_task_requirements() {
542        let mut spec1 = create_test_spec(
543            "spec-1",
544            "Feature One",
545            SpecStatus::Draft,
546            SpecPhase::Requirements,
547        );
548        spec1.requirements = vec![crate::models::Requirement {
549            id: "REQ-1".to_string(),
550            user_story: "Test".to_string(),
551            acceptance_criteria: vec![],
552            priority: Priority::Must,
553        }];
554
555        let mut spec2 = create_test_spec(
556            "spec-2",
557            "Feature Two",
558            SpecStatus::Approved,
559            SpecPhase::Design,
560        );
561        spec2.tasks = vec![crate::models::Task {
562            id: "1".to_string(),
563            description: "Task 1".to_string(),
564            subtasks: vec![],
565            requirements: vec!["REQ-1".to_string()],
566            status: crate::models::TaskStatus::NotStarted,
567            optional: false,
568        }];
569
570        let all_specs = vec![spec1, spec2.clone()];
571        let deps = SpecQueryEngine::resolve_dependencies(&spec2, &all_specs);
572        assert_eq!(deps.len(), 1);
573        assert_eq!(deps[0], "spec-1");
574    }
575
576    // ============================================================================
577    // Circular Dependency Detection Tests
578    // ============================================================================
579
580    #[test]
581    fn test_detect_circular_dependencies_no_cycles() {
582        let specs = vec![
583            create_test_spec(
584                "spec-1",
585                "Feature One",
586                SpecStatus::Draft,
587                SpecPhase::Requirements,
588            ),
589            create_test_spec(
590                "spec-2",
591                "Feature Two",
592                SpecStatus::Approved,
593                SpecPhase::Design,
594            ),
595        ];
596        let cycles = SpecQueryEngine::detect_circular_dependencies(&specs);
597        assert_eq!(cycles.len(), 0);
598    }
599
600    #[test]
601    fn test_query_consistency_multiple_executions() {
602        let specs = vec![
603            create_test_spec(
604                "spec-1",
605                "Feature One",
606                SpecStatus::Draft,
607                SpecPhase::Requirements,
608            ),
609            create_test_spec(
610                "spec-2",
611                "Feature Two",
612                SpecStatus::Approved,
613                SpecPhase::Design,
614            ),
615        ];
616        let query = SpecQuery {
617            status: Some(SpecStatus::Approved),
618            ..Default::default()
619        };
620
621        let results1 = SpecQueryEngine::query(&specs, &query);
622        let results2 = SpecQueryEngine::query(&specs, &query);
623
624        assert_eq!(results1.len(), results2.len());
625        for (r1, r2) in results1.iter().zip(results2.iter()) {
626            assert_eq!(r1.id, r2.id);
627        }
628    }
629}