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