1use std::collections::HashMap;
2use std::path::Path;
3
4use serde::{Deserialize, Serialize};
5
6use crate::error::ArgusError;
7use crate::types::Severity;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct Rule {
28 pub name: String,
30 pub severity: String,
32 pub description: String,
34}
35
36#[derive(Debug, Clone, Default, Serialize, Deserialize)]
49pub struct ArgusConfig {
50 #[serde(default)]
52 pub llm: LlmConfig,
53 #[serde(default)]
55 pub review: ReviewConfig,
56 #[serde(default)]
58 pub embedding: EmbeddingConfig,
59 #[serde(default)]
61 pub paths: HashMap<String, PathConfig>,
62 #[serde(default)]
64 pub rules: Vec<Rule>,
65}
66
67impl ArgusConfig {
68 pub fn from_file(path: &Path) -> Result<Self, ArgusError> {
84 let content = std::fs::read_to_string(path)?;
85 Self::from_toml(&content)
86 }
87
88 pub fn from_toml(content: &str) -> Result<Self, ArgusError> {
107 let config: Self = toml::from_str(content)?;
108 Ok(config)
109 }
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct LlmConfig {
124 #[serde(default = "default_provider")]
126 pub provider: String,
127 #[serde(default = "default_model")]
129 pub model: String,
130 pub api_key: Option<String>,
132 pub base_url: Option<String>,
134 pub max_input_tokens: Option<usize>,
136}
137
138fn default_provider() -> String {
139 "openai".into()
140}
141
142fn default_model() -> String {
143 "gpt-4o".into()
144}
145
146impl Default for LlmConfig {
147 fn default() -> Self {
148 Self {
149 provider: default_provider(),
150 model: default_model(),
151 api_key: None,
152 base_url: None,
153 max_input_tokens: None,
154 }
155 }
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct ReviewConfig {
176 #[serde(default = "default_max_comments")]
178 pub max_comments: usize,
179 #[serde(default = "default_min_confidence")]
181 pub min_confidence: f64,
182 #[serde(default = "default_severity_filter")]
184 pub severity_filter: Vec<Severity>,
185 #[serde(default)]
187 pub skip_patterns: Vec<String>,
188 #[serde(default)]
190 pub skip_extensions: Vec<String>,
191 #[serde(default = "default_max_diff_tokens")]
193 pub max_diff_tokens: usize,
194 #[serde(default)]
196 pub include_suggestions: bool,
197 #[serde(default = "default_cross_file")]
199 pub cross_file: bool,
200 #[serde(default = "default_self_reflection")]
205 pub self_reflection: bool,
206 #[serde(default = "default_self_reflection_score_threshold")]
208 pub self_reflection_score_threshold: u8,
209}
210
211fn default_max_comments() -> usize {
212 5
213}
214
215fn default_min_confidence() -> f64 {
216 90.0
217}
218
219fn default_severity_filter() -> Vec<Severity> {
220 vec![Severity::Bug, Severity::Warning]
221}
222
223fn default_max_diff_tokens() -> usize {
224 4000
225}
226
227fn default_cross_file() -> bool {
228 true
229}
230
231fn default_self_reflection() -> bool {
232 true
233}
234
235fn default_self_reflection_score_threshold() -> u8 {
236 7
237}
238
239impl Default for ReviewConfig {
240 fn default() -> Self {
241 Self {
242 max_comments: default_max_comments(),
243 min_confidence: default_min_confidence(),
244 severity_filter: default_severity_filter(),
245 skip_patterns: Vec::new(),
246 skip_extensions: Vec::new(),
247 max_diff_tokens: default_max_diff_tokens(),
248 include_suggestions: false,
249 cross_file: default_cross_file(),
250 self_reflection: default_self_reflection(),
251 self_reflection_score_threshold: default_self_reflection_score_threshold(),
252 }
253 }
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct PathConfig {
271 pub instructions: Option<String>,
273 #[serde(default)]
275 pub context_boundary: bool,
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct EmbeddingConfig {
292 #[serde(default = "default_embedding_provider")]
294 pub provider: String,
295 pub api_key: Option<String>,
297 #[serde(default = "default_embedding_model")]
299 pub model: String,
300 #[serde(default = "default_embedding_dimensions")]
302 pub dimensions: usize,
303}
304
305fn default_embedding_provider() -> String {
306 "voyage".into()
307}
308
309fn default_embedding_model() -> String {
310 "voyage-code-3".into()
311}
312
313fn default_embedding_dimensions() -> usize {
314 1024
315}
316
317impl Default for EmbeddingConfig {
318 fn default() -> Self {
319 Self {
320 provider: default_embedding_provider(),
321 api_key: None,
322 model: default_embedding_model(),
323 dimensions: default_embedding_dimensions(),
324 }
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 #[test]
333 fn default_config_has_expected_values() {
334 let config = ArgusConfig::default();
335 assert_eq!(config.review.max_comments, 5);
336 assert_eq!(config.review.min_confidence, 90.0);
337 assert_eq!(config.review.max_diff_tokens, 4000);
338 assert!(!config.review.include_suggestions);
339 assert!(config.review.skip_patterns.is_empty());
340 assert!(config.review.skip_extensions.is_empty());
341 assert_eq!(config.llm.provider, "openai");
342 assert_eq!(config.llm.model, "gpt-4o");
343 assert_eq!(config.embedding.provider, "voyage");
344 assert_eq!(config.embedding.model, "voyage-code-3");
345 assert_eq!(config.embedding.dimensions, 1024);
346 assert!(config.paths.is_empty());
347 assert!(config.review.self_reflection);
348 assert_eq!(config.review.self_reflection_score_threshold, 7);
349 }
350
351 #[test]
352 fn parse_minimal_toml() {
353 let toml = r#"
354[review]
355max_comments = 10
356min_confidence = 85.0
357"#;
358 let config = ArgusConfig::from_toml(toml).unwrap();
359 assert_eq!(config.review.max_comments, 10);
360 assert_eq!(config.review.min_confidence, 85.0);
361 }
362
363 #[test]
364 fn parse_full_toml() {
365 let toml = r#"
366[llm]
367provider = "anthropic"
368model = "claude-sonnet-4-20250514"
369base_url = "https://api.anthropic.com"
370max_input_tokens = 50000
371
372[review]
373max_comments = 3
374min_confidence = 95.0
375severity_filter = ["bug"]
376
377[paths."packages/auth"]
378instructions = "Focus on authentication flows"
379context_boundary = true
380"#;
381 let config = ArgusConfig::from_toml(toml).unwrap();
382 assert_eq!(config.llm.provider, "anthropic");
383 assert_eq!(config.llm.max_input_tokens, Some(50000));
384 assert_eq!(config.review.max_comments, 3);
385 assert_eq!(config.review.severity_filter, vec![Severity::Bug]);
386
387 let auth_path = &config.paths["packages/auth"];
388 assert!(auth_path.context_boundary);
389 assert_eq!(
390 auth_path.instructions.as_deref(),
391 Some("Focus on authentication flows")
392 );
393 }
394
395 #[test]
396 fn empty_toml_gives_defaults() {
397 let config = ArgusConfig::from_toml("").unwrap();
398 assert_eq!(config.review.max_comments, 5);
399 assert_eq!(config.llm.model, "gpt-4o");
400 }
401
402 #[test]
403 fn invalid_toml_returns_error() {
404 let result = ArgusConfig::from_toml("{{invalid}}");
405 assert!(result.is_err());
406 }
407
408 #[test]
409 fn parse_noise_reduction_config() {
410 let toml = r#"
411[review]
412max_comments = 3
413skip_patterns = ["*.test.ts", "fixtures/**"]
414skip_extensions = ["snap", "lock"]
415max_diff_tokens = 8000
416include_suggestions = true
417"#;
418 let config = ArgusConfig::from_toml(toml).unwrap();
419 assert_eq!(config.review.max_comments, 3);
420 assert_eq!(
421 config.review.skip_patterns,
422 vec!["*.test.ts", "fixtures/**"]
423 );
424 assert_eq!(config.review.skip_extensions, vec!["snap", "lock"]);
425 assert_eq!(config.review.max_diff_tokens, 8000);
426 assert!(config.review.include_suggestions);
427 }
428
429 #[test]
430 fn noise_reduction_defaults_when_omitted() {
431 let toml = r#"
432[review]
433max_comments = 10
434"#;
435 let config = ArgusConfig::from_toml(toml).unwrap();
436 assert!(config.review.skip_patterns.is_empty());
437 assert!(config.review.skip_extensions.is_empty());
438 assert_eq!(config.review.max_diff_tokens, 4000);
439 assert!(!config.review.include_suggestions);
440 }
441
442 #[test]
443 fn parse_rules_from_toml() {
444 let toml = r#"
445[[rules]]
446name = "no-unwrap"
447severity = "warning"
448description = "Do not use .unwrap() in production code"
449
450[[rules]]
451name = "no-todo"
452severity = "suggestion"
453description = "Remove TODO comments before merging"
454"#;
455 let config = ArgusConfig::from_toml(toml).unwrap();
456 assert_eq!(config.rules.len(), 2);
457 assert_eq!(config.rules[0].name, "no-unwrap");
458 assert_eq!(config.rules[0].severity, "warning");
459 assert_eq!(
460 config.rules[0].description,
461 "Do not use .unwrap() in production code"
462 );
463 assert_eq!(config.rules[1].name, "no-todo");
464 assert_eq!(config.rules[1].severity, "suggestion");
465 }
466
467 #[test]
468 fn empty_rules_by_default() {
469 assert!(ArgusConfig::default().rules.is_empty());
470 }
471}