Skip to main content

aptu_core/config/
review.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! PR review prompt configuration.
4
5use serde::{Deserialize, Serialize};
6
7/// PR review prompt configuration.
8///
9/// Controls prompt token budgets and GitHub API constraints for PR reviews:
10///
11/// - `max_prompt_chars`: 120,000 chars is a conservative budget below common LLM context
12///   window limits (e.g., 128k token models), accounting for system prompt and response overhead.
13/// - `max_full_content_files`: 10 files caps GitHub Contents API calls per review to limit
14///   latency and rate limit usage.
15/// - `max_chars_per_file`: 32,000 chars per file gives adequate context for most files
16///   without dominating the prompt budget; budget drop logic trims below 120k if needed.
17/// - `max_instructions_chars`: 1,500 chars caps repository instructions to prevent prompt bloat.
18/// - `max_diff_chars`: 200,000 chars caps the total diff content across all files in the prompt.
19/// - `max_patch_chars_per_file`: 25,000 chars caps each individual file patch before dropping it entirely.
20#[derive(Debug, Deserialize, Serialize, Clone)]
21#[serde(default)]
22pub struct ReviewConfig {
23    /// Maximum total prompt character budget (default: `120_000`).
24    pub max_prompt_chars: usize,
25    /// Maximum number of files to fetch full content for (default: 10).
26    pub max_full_content_files: usize,
27    /// Maximum characters per file's full content (default: `32_000`).
28    pub max_chars_per_file: usize,
29    /// Maximum total diff characters across all files in the prompt (default: `200_000`).
30    pub max_diff_chars: usize,
31    /// Maximum characters per individual file patch before the patch is dropped entirely (default: `25_000`).
32    pub max_patch_chars_per_file: usize,
33    /// Maximum characters for repository instructions (default: `1_500`).
34    #[serde(default = "default_max_instructions_chars")]
35    pub max_instructions_chars: usize,
36    /// Optional path to repository instructions file (overrides default AGENTS.md and .github/instructions/pr-review.md).
37    #[serde(default)]
38    pub instructions_file: Option<String>,
39    /// Minimum remaining prompt budget to auto-enable call graph (default: `20_000`).
40    ///
41    /// The call graph is built only when:
42    /// `budget_remaining = max_prompt_chars - estimated_size (call_graph excluded)`
43    /// and `budget_remaining > min_budget_for_call_graph`.
44    ///
45    /// A value >= `max_prompt_chars` means call graph is never auto-enabled.
46    /// A value > `max_prompt_chars / 2` means call graph is only built for
47    /// the largest diffs — consider lowering the threshold.
48    #[serde(default = "default_min_budget_for_call_graph")]
49    pub min_budget_for_call_graph: usize,
50    /// Maximum characters for dependency release notes (default: `2_000`).
51    #[serde(default = "default_max_dep_release_chars")]
52    pub max_dep_release_chars: usize,
53    /// Maximum number of dependency packages to enrich (default: 3).
54    #[serde(default = "default_max_dep_packages")]
55    pub max_dep_packages: usize,
56}
57
58fn default_max_instructions_chars() -> usize {
59    1_500
60}
61
62fn default_min_budget_for_call_graph() -> usize {
63    20_000
64}
65
66fn default_max_dep_release_chars() -> usize {
67    2_000
68}
69
70fn default_max_dep_packages() -> usize {
71    3
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn test_validate_consistency_ok() {
80        let config = ReviewConfig::default();
81        let warnings = config.validate_consistency();
82        assert!(
83            warnings.is_empty(),
84            "default config should produce no warnings: {warnings:?}"
85        );
86    }
87
88    #[test]
89    fn test_validate_consistency_threshold_equals_max() {
90        let config = ReviewConfig {
91            min_budget_for_call_graph: 120_000,
92            max_prompt_chars: 120_000,
93            ..ReviewConfig::default()
94        };
95        let warnings = config.validate_consistency();
96        assert_eq!(warnings.len(), 1, "should produce exactly 1 warning");
97        assert!(
98            warnings[0].contains("call_graph will never be built"),
99            "warning should indicate call_graph is never built: {}",
100            warnings[0]
101        );
102    }
103
104    #[test]
105    fn test_validate_consistency_threshold_over_half() {
106        let config = ReviewConfig {
107            min_budget_for_call_graph: 80_000,
108            max_prompt_chars: 120_000,
109            ..ReviewConfig::default()
110        };
111        let warnings = config.validate_consistency();
112        assert_eq!(warnings.len(), 1, "should produce exactly 1 warning");
113        assert!(
114            warnings[0].contains("only be built for the largest diffs"),
115            "warning should indicate call_graph rarely enables: {}",
116            warnings[0]
117        );
118    }
119
120    #[test]
121    fn test_default_max_patch_chars_per_file() {
122        let config = ReviewConfig::default();
123        assert_eq!(
124            config.max_patch_chars_per_file, 25_000,
125            "default max_patch_chars_per_file should be 25_000"
126        );
127    }
128}
129
130impl Default for ReviewConfig {
131    fn default() -> Self {
132        Self {
133            max_prompt_chars: 120_000,
134            max_full_content_files: 10,
135            max_chars_per_file: 32_000,
136            max_diff_chars: 200_000,
137            max_patch_chars_per_file: 25_000,
138            max_instructions_chars: 1_500,
139            instructions_file: None,
140            min_budget_for_call_graph: 20_000,
141            max_dep_release_chars: 2_000,
142            max_dep_packages: 3,
143        }
144    }
145}
146
147impl ReviewConfig {
148    /// Validate internal consistency of review configuration.
149    ///
150    /// Returns a list of warning strings for any misconfigured values.
151    /// The caller should emit these warnings via `tracing::warn!` or similar.
152    #[must_use]
153    pub fn validate_consistency(&self) -> Vec<String> {
154        let mut warnings = Vec::new();
155
156        // Warning 1: min_budget_for_call_graph >= max_prompt_chars means
157        // call_graph is never auto-enabled (budget_remaining is always <= max_prompt_chars
158        // and must be > min_budget_for_call_graph).
159        if self.min_budget_for_call_graph >= self.max_prompt_chars {
160            warnings.push(format!(
161                "min_budget_for_call_graph ({}) >= max_prompt_chars ({}): call_graph will never be built; call_graph is enabled only when budget_remaining > min_budget_for_call_graph",
162                self.min_budget_for_call_graph, self.max_prompt_chars
163            ));
164        }
165        // Warning 2: min_budget_for_call_graph > max_prompt_chars / 2 but < max_prompt_chars
166        // means call_graph will only be built for the largest diffs.
167        else if self.min_budget_for_call_graph > self.max_prompt_chars / 2 {
168            warnings.push(format!(
169                "min_budget_for_call_graph ({}) exceeds half of max_prompt_chars ({}): call_graph will only be built for the largest diffs; consider lowering the threshold",
170                self.min_budget_for_call_graph, self.max_prompt_chars
171            ));
172        }
173
174        warnings
175    }
176}