Skip to main content

aptu_core/config/
graph.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Structural graph configuration for PR review context.
4
5use serde::{Deserialize, Serialize};
6
7/// Structural graph configuration.
8///
9/// Controls whether the petgraph-backed call graph is built for PR review
10/// context, how long cached graphs remain valid, and the maximum blast-radius
11/// subgraph size injected into the prompt:
12///
13/// - `enabled`: disabled by default; opt-in via config or CLI flag.
14/// - `cache_ttl_hours`: 24 hours balances staleness against rebuild cost for
15///   repositories with frequent commits.
16/// - `max_nodes`: 50,000 nodes caps the blast-radius subgraph and cache size
17///   for very large repositories.
18/// - `max_depth`: 4 hops caps the blast-radius BFS traversal depth.
19#[derive(Debug, Deserialize, Serialize, Clone)]
20#[serde(default)]
21pub struct GraphConfig {
22    /// Whether structural graph context is enabled (default: `false`).
23    #[serde(default = "default_enabled")]
24    pub enabled: bool,
25    /// Cache time-to-live in hours before a cached graph is rebuilt (default: `24`).
26    #[serde(default = "default_cache_ttl_hours")]
27    pub cache_ttl_hours: u64,
28    /// Maximum number of nodes in the blast-radius subgraph (default: `50_000`).
29    #[serde(default = "default_max_nodes")]
30    pub max_nodes: usize,
31    /// Maximum BFS hop depth from a modified node in the blast-radius subgraph (default: `4`).
32    #[serde(default = "default_max_depth")]
33    pub max_depth: usize,
34}
35
36fn default_enabled() -> bool {
37    false
38}
39
40fn default_cache_ttl_hours() -> u64 {
41    24
42}
43
44fn default_max_nodes() -> usize {
45    50_000
46}
47
48fn default_max_depth() -> usize {
49    4
50}
51
52impl Default for GraphConfig {
53    fn default() -> Self {
54        Self {
55            enabled: default_enabled(),
56            cache_ttl_hours: default_cache_ttl_hours(),
57            max_nodes: default_max_nodes(),
58            max_depth: default_max_depth(),
59        }
60    }
61}
62
63impl GraphConfig {
64    /// Validate internal consistency of graph configuration.
65    ///
66    /// Returns a list of warning strings for any misconfigured values.
67    /// The caller should emit these warnings via `tracing::warn!` or similar.
68    #[must_use]
69    pub fn validate_consistency(&self) -> Vec<String> {
70        let mut warnings = Vec::new();
71
72        if self.enabled && self.max_nodes == 0 {
73            warnings.push(
74                "max_nodes is 0 while graph is enabled: blast-radius subgraph will always be empty"
75                    .to_string(),
76            );
77        }
78
79        if self.enabled && self.max_depth == 0 {
80            warnings.push(
81                "max_depth is 0 while graph is enabled: blast-radius subgraph will always be empty"
82                    .to_string(),
83            );
84        }
85
86        if self.enabled && self.cache_ttl_hours == 0 {
87            warnings.push(
88                "cache_ttl_hours is 0 while graph is enabled: cache is rebuilt on every review"
89                    .to_string(),
90            );
91        }
92
93        warnings
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn test_default_disabled() {
103        let config = GraphConfig::default();
104        assert!(!config.enabled, "graph should be disabled by default");
105        assert_eq!(config.cache_ttl_hours, 24);
106        assert_eq!(config.max_nodes, 50_000);
107    }
108
109    #[test]
110    fn test_validate_consistency_ok() {
111        let config = GraphConfig::default();
112        let warnings = config.validate_consistency();
113        assert!(
114            warnings.is_empty(),
115            "default config should produce no warnings: {:?}",
116            warnings
117        );
118    }
119
120    #[test]
121    fn test_validate_consistency_zero_max_nodes_enabled() {
122        let config = GraphConfig {
123            enabled: true,
124            max_nodes: 0,
125            ..GraphConfig::default()
126        };
127        let warnings = config.validate_consistency();
128        assert_eq!(warnings.len(), 1, "should produce exactly 1 warning");
129        assert!(warnings[0].contains("max_nodes is 0"));
130    }
131
132    #[test]
133    fn test_deserializes_from_toml_with_missing_fields() {
134        let toml_str = "";
135        let config: GraphConfig = toml::from_str(toml_str).unwrap();
136        assert!(!config.enabled);
137        assert_eq!(config.cache_ttl_hours, 24);
138        assert_eq!(config.max_nodes, 50_000);
139    }
140}