1use super::*;
12
13pub const AST_GREP_PINNED_VERSION: &str = "0.45.3";
17pub const MUR_TOOLS_DIR: &str = "tools";
20const AST_GREP_TOOL_DIR: &str = "ast-grep";
21const AST_GREP_BIN_STEM: &str = "ast-grep";
22
23pub fn ast_grep_binary_path(mur_home: &std::path::Path) -> PathBuf {
27 mur_home
28 .join(MUR_TOOLS_DIR)
29 .join(AST_GREP_TOOL_DIR)
30 .join(AST_GREP_PINNED_VERSION)
31 .join(format!(
32 "{AST_GREP_BIN_STEM}{}",
33 std::env::consts::EXE_SUFFIX
34 ))
35}
36pub const AST_GREP_DEFAULT_MAX_RESULTS: u32 = 200;
38pub const AST_GREP_DEFAULT_MAX_CONTEXT_LINES: u32 = 3;
40pub const AST_GREP_DEFAULT_TIMEOUT_SECS: u64 = 30;
42pub const AST_GREP_DEFAULT_MAX_OUTPUT_BYTES: u64 = 1024 * 1024;
44pub const AST_GREP_DEFAULT_MAX_MATCH_BYTES: u64 = 2 * 1024;
46
47pub const AST_GREP_HARD_MAX_RESULTS: u32 = 2_000;
49pub const AST_GREP_HARD_MAX_CONTEXT_LINES: u32 = 20;
51pub const AST_GREP_HARD_MAX_TIMEOUT_SECS: u64 = 300;
53pub const AST_GREP_HARD_MAX_OUTPUT_BYTES: u64 = 16 * 1024 * 1024;
55pub const AST_GREP_HARD_MAX_MATCH_BYTES: u64 = 64 * 1024;
57
58#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
59pub struct SearchConfig {
60 #[serde(default)]
61 pub ast_grep: AstGrepConfig,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub struct AstGrepConfig {
68 #[serde(default = "ast_grep_default_max_results")]
69 pub max_results: u32,
70 #[serde(default = "ast_grep_default_max_context_lines")]
71 pub max_context_lines: u32,
72 #[serde(default = "ast_grep_default_timeout_secs")]
73 pub timeout_secs: u64,
74 #[serde(default = "ast_grep_default_max_output_bytes")]
75 pub max_output_bytes: u64,
76 #[serde(default = "ast_grep_default_max_match_bytes")]
77 pub max_match_bytes: u64,
78}
79
80fn ast_grep_default_max_results() -> u32 {
81 AST_GREP_DEFAULT_MAX_RESULTS
82}
83fn ast_grep_default_max_context_lines() -> u32 {
84 AST_GREP_DEFAULT_MAX_CONTEXT_LINES
85}
86fn ast_grep_default_timeout_secs() -> u64 {
87 AST_GREP_DEFAULT_TIMEOUT_SECS
88}
89fn ast_grep_default_max_output_bytes() -> u64 {
90 AST_GREP_DEFAULT_MAX_OUTPUT_BYTES
91}
92fn ast_grep_default_max_match_bytes() -> u64 {
93 AST_GREP_DEFAULT_MAX_MATCH_BYTES
94}
95
96impl Default for AstGrepConfig {
97 fn default() -> Self {
98 Self {
99 max_results: AST_GREP_DEFAULT_MAX_RESULTS,
100 max_context_lines: AST_GREP_DEFAULT_MAX_CONTEXT_LINES,
101 timeout_secs: AST_GREP_DEFAULT_TIMEOUT_SECS,
102 max_output_bytes: AST_GREP_DEFAULT_MAX_OUTPUT_BYTES,
103 max_match_bytes: AST_GREP_DEFAULT_MAX_MATCH_BYTES,
104 }
105 }
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub struct AstGrepLimits {
111 pub max_results: u32,
112 pub context_lines: u32,
113 pub timeout_secs: u64,
114 pub max_output_bytes: u64,
115 pub max_match_bytes: u64,
116}
117
118fn clamp<T: Ord + Copy>(requested: Option<T>, configured: T, hard: T, floor: T) -> T {
122 let cap = configured.min(hard);
123 requested.map_or(cap, |r| r.min(cap)).max(floor)
124}
125
126impl AstGrepConfig {
127 pub fn resolve(&self, max_results: Option<u32>, context: Option<u32>) -> AstGrepLimits {
131 AstGrepLimits {
132 max_results: clamp(max_results, self.max_results, AST_GREP_HARD_MAX_RESULTS, 1),
133 context_lines: clamp(
134 context,
135 self.max_context_lines,
136 AST_GREP_HARD_MAX_CONTEXT_LINES,
137 0,
138 ),
139 timeout_secs: clamp(None, self.timeout_secs, AST_GREP_HARD_MAX_TIMEOUT_SECS, 1),
140 max_output_bytes: clamp(
141 None,
142 self.max_output_bytes,
143 AST_GREP_HARD_MAX_OUTPUT_BYTES,
144 1,
145 ),
146 max_match_bytes: clamp(None, self.max_match_bytes, AST_GREP_HARD_MAX_MATCH_BYTES, 1),
147 }
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn config_without_search_block_uses_defaults() {
157 let c: Config = serde_yaml::from_str("llm: {}\n").unwrap();
158 assert_eq!(c.search.ast_grep, AstGrepConfig::default());
159 }
160
161 #[test]
162 fn partial_ast_grep_block_keeps_omitted_defaults() {
163 let c: Config =
164 serde_yaml::from_str("search:\n ast_grep:\n max_results: 50\n").unwrap();
165 let g = &c.search.ast_grep;
166 assert_eq!(g.max_results, 50);
167 assert_eq!(g.max_context_lines, AST_GREP_DEFAULT_MAX_CONTEXT_LINES);
168 assert_eq!(g.timeout_secs, AST_GREP_DEFAULT_TIMEOUT_SECS);
169 assert_eq!(g.max_output_bytes, AST_GREP_DEFAULT_MAX_OUTPUT_BYTES);
170 assert_eq!(g.max_match_bytes, AST_GREP_DEFAULT_MAX_MATCH_BYTES);
171 }
172
173 #[test]
174 fn defaults_sit_under_ceilings() {
175 let l = AstGrepConfig::default().resolve(None, None);
176 assert_eq!(l.max_results, AST_GREP_DEFAULT_MAX_RESULTS);
177 assert_eq!(l.context_lines, AST_GREP_DEFAULT_MAX_CONTEXT_LINES);
178 assert_eq!(l.timeout_secs, AST_GREP_DEFAULT_TIMEOUT_SECS);
179 assert_eq!(l.max_output_bytes, AST_GREP_DEFAULT_MAX_OUTPUT_BYTES);
180 assert_eq!(l.max_match_bytes, AST_GREP_DEFAULT_MAX_MATCH_BYTES);
181 }
182
183 #[test]
184 fn agent_request_cannot_exceed_config() {
185 let l = AstGrepConfig::default().resolve(Some(u32::MAX), Some(u32::MAX));
186 assert_eq!(l.max_results, AST_GREP_DEFAULT_MAX_RESULTS);
187 assert_eq!(l.context_lines, AST_GREP_DEFAULT_MAX_CONTEXT_LINES);
188 }
189
190 #[test]
191 fn agent_request_below_config_wins() {
192 let l = AstGrepConfig::default().resolve(Some(5), Some(0));
193 assert_eq!((l.max_results, l.context_lines), (5, 0));
194 }
195
196 #[test]
197 fn config_cannot_exceed_hard_ceiling() {
198 let c = AstGrepConfig {
199 max_results: u32::MAX,
200 max_context_lines: u32::MAX,
201 timeout_secs: u64::MAX,
202 max_output_bytes: u64::MAX,
203 max_match_bytes: u64::MAX,
204 };
205 let l = c.resolve(None, None);
206 assert_eq!(l.max_results, AST_GREP_HARD_MAX_RESULTS);
207 assert_eq!(l.context_lines, AST_GREP_HARD_MAX_CONTEXT_LINES);
208 assert_eq!(l.timeout_secs, AST_GREP_HARD_MAX_TIMEOUT_SECS);
209 assert_eq!(l.max_output_bytes, AST_GREP_HARD_MAX_OUTPUT_BYTES);
210 assert_eq!(l.max_match_bytes, AST_GREP_HARD_MAX_MATCH_BYTES);
211 }
212
213 #[test]
214 fn zero_config_is_floored_not_a_silent_noop() {
215 let c = AstGrepConfig {
216 max_results: 0,
217 max_context_lines: 0,
218 timeout_secs: 0,
219 max_output_bytes: 0,
220 max_match_bytes: 0,
221 };
222 let l = c.resolve(Some(0), None);
223 assert_eq!(l.max_results, 1);
224 assert_eq!(l.context_lines, 0);
225 assert_eq!(l.timeout_secs, 1);
226 assert_eq!(l.max_output_bytes, 1);
227 assert_eq!(l.max_match_bytes, 1);
228 }
229}