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}