Skip to main content

sqry_core/query/
cycles_config.rs

1//! Configuration types for cycle detection.
2//!
3//! These types configure cycle-detection queries. The actual cycle detection
4//! is implemented in `sqry-db` via
5//! [`sqry_db::queries::CyclesQuery`](../../../sqry_db/queries/struct.CyclesQuery.html)
6//! and [`sqry_db::queries::IsInCycleQuery`](../../../sqry_db/queries/struct.IsInCycleQuery.html),
7//! which consume [`CircularType`] directly and translate [`CircularConfig`]
8//! into [`sqry_db::queries::CycleBounds`](../../../sqry_db/queries/struct.CycleBounds.html).
9//!
10//! The legacy `find_all_cycles_graph` / `is_node_in_cycle` functions were
11//! removed in Phase 3C DB19 (2026-04-15) — all cycle detection routes
12//! through sqry-db so one Tarjan SCC pass is shared across MCP / CLI / LSP
13//! callers on the same snapshot. The types below remain because they are
14//! consumed by `sqry-db` as public key fragments.
15
16/// Type of circular dependency to detect.
17// Serialize/Deserialize added for PN3 cold-start persistence: CircularType is
18// used as a field in sqry-db CyclesKey / IsInCycleKey, which are postcard-serialized
19// at cache-insert time.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
21pub enum CircularType {
22    /// Function/method call cycles (A calls B calls C calls A).
23    Calls,
24    /// File import cycles (a.rs imports b.rs imports a.rs).
25    Imports,
26    /// Module-level cycles (aggregated from import graph).
27    Modules,
28}
29
30impl CircularType {
31    /// Parse circular type from query value string.
32    ///
33    /// Accepts the canonical plural form (`calls`, `imports`, `modules`) and
34    /// the legacy singular aliases (`call`, `import`, `module`).
35    #[must_use]
36    pub fn try_parse(s: &str) -> Option<Self> {
37        match s.to_lowercase().as_str() {
38            "calls" | "call" => Some(Self::Calls),
39            "imports" | "import" => Some(Self::Imports),
40            "modules" | "module" => Some(Self::Modules),
41            _ => None,
42        }
43    }
44}
45
46/// Configuration for circular dependency detection.
47///
48/// Mirrors the sqry-db [`sqry_db::queries::CycleBounds`](../../../sqry_db/queries/struct.CycleBounds.html)
49/// type. Handlers construct a `CircularConfig` from user CLI/MCP flags and
50/// pass it through to the sqry-db key.
51#[derive(Debug, Clone)]
52pub struct CircularConfig {
53    /// Minimum cycle depth to report (default: 2).
54    pub min_depth: usize,
55    /// Maximum cycle depth to report (default: unbounded).
56    pub max_depth: Option<usize>,
57    /// Maximum results to return (default: 100).
58    pub max_results: usize,
59    /// Include self-loops (A -> A) in results (default: false).
60    pub should_include_self_loops: bool,
61}
62
63impl Default for CircularConfig {
64    fn default() -> Self {
65        Self {
66            min_depth: 2,
67            max_depth: None,
68            max_results: 100,
69            should_include_self_loops: false,
70        }
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn parses_canonical_plural_forms() {
80        assert_eq!(CircularType::try_parse("calls"), Some(CircularType::Calls));
81        assert_eq!(
82            CircularType::try_parse("imports"),
83            Some(CircularType::Imports)
84        );
85        assert_eq!(
86            CircularType::try_parse("modules"),
87            Some(CircularType::Modules)
88        );
89    }
90
91    #[test]
92    fn parses_legacy_singular_aliases() {
93        assert_eq!(CircularType::try_parse("call"), Some(CircularType::Calls));
94        assert_eq!(
95            CircularType::try_parse("import"),
96            Some(CircularType::Imports)
97        );
98        assert_eq!(
99            CircularType::try_parse("module"),
100            Some(CircularType::Modules)
101        );
102    }
103
104    #[test]
105    fn is_case_insensitive() {
106        assert_eq!(CircularType::try_parse("CALLS"), Some(CircularType::Calls));
107        assert_eq!(
108            CircularType::try_parse("Imports"),
109            Some(CircularType::Imports)
110        );
111    }
112
113    #[test]
114    fn rejects_unknown_types() {
115        assert_eq!(CircularType::try_parse("unknown"), None);
116        assert_eq!(CircularType::try_parse(""), None);
117    }
118
119    #[test]
120    fn default_config_matches_legacy() {
121        let config = CircularConfig::default();
122        assert_eq!(config.min_depth, 2);
123        assert_eq!(config.max_depth, None);
124        assert_eq!(config.max_results, 100);
125        assert!(!config.should_include_self_loops);
126    }
127}