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#[derive(Debug, Deserialize, Serialize, Clone)]
19#[serde(default)]
20pub struct GraphConfig {
21    /// Whether structural graph context is enabled (default: `false`).
22    #[serde(default = "default_enabled")]
23    pub enabled: bool,
24    /// Cache time-to-live in hours before a cached graph is rebuilt (default: `24`).
25    #[serde(default = "default_cache_ttl_hours")]
26    pub cache_ttl_hours: u64,
27    /// Maximum number of nodes in the blast-radius subgraph (default: `50_000`).
28    #[serde(default = "default_max_nodes")]
29    pub max_nodes: usize,
30}
31
32fn default_enabled() -> bool {
33    false
34}
35
36fn default_cache_ttl_hours() -> u64 {
37    24
38}
39
40fn default_max_nodes() -> usize {
41    50_000
42}
43
44impl Default for GraphConfig {
45    fn default() -> Self {
46        Self {
47            enabled: default_enabled(),
48            cache_ttl_hours: default_cache_ttl_hours(),
49            max_nodes: default_max_nodes(),
50        }
51    }
52}
53
54impl GraphConfig {
55    /// Validate internal consistency of graph configuration.
56    ///
57    /// Returns a list of warning strings for any misconfigured values.
58    /// The caller should emit these warnings via `tracing::warn!` or similar.
59    #[must_use]
60    pub fn validate_consistency(&self) -> Vec<String> {
61        let mut warnings = Vec::new();
62
63        if self.enabled && self.max_nodes == 0 {
64            warnings.push(
65                "max_nodes is 0 while graph is enabled: blast-radius subgraph will always be empty"
66                    .to_string(),
67            );
68        }
69
70        if self.enabled && self.cache_ttl_hours == 0 {
71            warnings.push(
72                "cache_ttl_hours is 0 while graph is enabled: cache is rebuilt on every review"
73                    .to_string(),
74            );
75        }
76
77        warnings
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn test_default_disabled() {
87        let config = GraphConfig::default();
88        assert!(!config.enabled, "graph should be disabled by default");
89        assert_eq!(config.cache_ttl_hours, 24);
90        assert_eq!(config.max_nodes, 50_000);
91    }
92
93    #[test]
94    fn test_validate_consistency_ok() {
95        let config = GraphConfig::default();
96        let warnings = config.validate_consistency();
97        assert!(
98            warnings.is_empty(),
99            "default config should produce no warnings: {:?}",
100            warnings
101        );
102    }
103
104    #[test]
105    fn test_validate_consistency_zero_max_nodes_enabled() {
106        let config = GraphConfig {
107            enabled: true,
108            max_nodes: 0,
109            ..GraphConfig::default()
110        };
111        let warnings = config.validate_consistency();
112        assert_eq!(warnings.len(), 1, "should produce exactly 1 warning");
113        assert!(warnings[0].contains("max_nodes is 0"));
114    }
115
116    #[test]
117    fn test_deserializes_from_toml_with_missing_fields() {
118        let toml_str = "";
119        let config: GraphConfig = toml::from_str(toml_str).unwrap();
120        assert!(!config.enabled);
121        assert_eq!(config.cache_ttl_hours, 24);
122        assert_eq!(config.max_nodes, 50_000);
123    }
124}