1use crate::domain::violations::{GuardianError, GuardianResult, Severity};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::fs;
12use std::path::Path;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct GuardianConfig {
17 pub version: String,
19 pub paths: PathConfig,
21 pub patterns: HashMap<String, PatternCategory>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct PathConfig {
28 pub patterns: Vec<String>,
30 pub ignore_file: Option<String>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct PatternCategory {
37 pub severity: Severity,
39 pub enabled: bool,
41 pub rules: Vec<PatternRule>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
47pub struct PatternRule {
48 pub id: String,
50 #[serde(rename = "type")]
52 pub rule_type: RuleType,
53 pub pattern: String,
55 pub message: String,
57 pub severity: Option<Severity>,
59 #[serde(default = "default_true")]
61 pub enabled: bool,
62 #[serde(default)]
64 pub case_sensitive: bool,
65 pub exclude_if: Option<ExcludeConditions>,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
71#[serde(rename_all = "snake_case")]
72pub enum RuleType {
73 Regex,
75 Ast,
77 Semantic,
79 ImportAnalysis,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
85pub struct ExcludeConditions {
86 pub attribute: Option<String>,
88 #[serde(default)]
90 pub in_tests: bool,
91 pub file_patterns: Option<Vec<String>>,
93}
94
95impl GuardianConfig {
96 pub fn load_from_file<P: AsRef<Path>>(path: P) -> GuardianResult<Self> {
98 let contents = fs::read_to_string(&path).map_err(|e| {
99 GuardianError::config(format!(
100 "Failed to read config file '{}': {}",
101 path.as_ref().display(),
102 e
103 ))
104 })?;
105
106 let config: Self = serde_yaml::from_str(&contents).map_err(|e| {
107 GuardianError::config(format!(
108 "Failed to parse config file '{}': {}",
109 path.as_ref().display(),
110 e
111 ))
112 })?;
113
114 config.validate()?;
115 Ok(config)
116 }
117
118 pub fn load_from_str(content: &str) -> GuardianResult<Self> {
120 let config: Self = serde_yaml::from_str(content)
121 .map_err(|e| GuardianError::config(format!("Failed to parse config: {e}")))?;
122
123 config.validate()?;
124 Ok(config)
125 }
126
127 pub fn with_defaults() -> Self {
129 Self {
130 version: "1.0".to_string(),
131 paths: PathConfig {
132 patterns: vec![
133 "target/".to_string(),
135 "**/node_modules/".to_string(),
136 "**/.git/".to_string(),
137 "**/*.generated.*".to_string(),
138 ],
139 ignore_file: Some(".guardianignore".to_string()),
140 },
141 patterns: Self::default_patterns(),
142 }
143 }
144
145 fn default_patterns() -> HashMap<String, PatternCategory> {
147 let mut patterns = HashMap::new();
148
149 patterns.insert(
151 "placeholders".to_string(),
152 PatternCategory {
153 severity: Severity::Error,
154 enabled: true,
155 rules: vec![
156 PatternRule {
157 id: "todo_comments".to_string(),
158 rule_type: RuleType::Regex,
159 pattern: build_development_marker_pattern(),
160 message: "Development marker detected: {match}".to_string(),
161 severity: None,
162 enabled: true,
163 case_sensitive: false,
164 exclude_if: None,
165 },
166 PatternRule {
167 id: "temporary_markers".to_string(),
168 rule_type: RuleType::Regex,
169 pattern: build_temporary_marker_pattern(),
170 message: "Implementation marker found: {match}".to_string(),
171 severity: None,
172 enabled: true,
173 case_sensitive: false,
174 exclude_if: Some(ExcludeConditions {
175 attribute: None,
176 in_tests: true,
177 file_patterns: Some(vec!["**/tests/**".to_string()]),
178 }),
179 },
180 PatternRule {
181 id: "unimplemented_macros".to_string(),
182 rule_type: RuleType::Ast,
183 pattern: build_unfinished_macro_pattern(),
184 message: "Unfinished macro {macro_name}! found".to_string(),
185 severity: None,
186 enabled: true,
187 case_sensitive: true,
188 exclude_if: Some(ExcludeConditions {
189 attribute: Some("#[test]".to_string()),
190 in_tests: true,
191 file_patterns: None,
192 }),
193 },
194 ],
195 },
196 );
197
198 patterns.insert(
200 "incomplete_implementations".to_string(),
201 PatternCategory {
202 severity: Severity::Error,
203 enabled: true,
204 rules: vec![PatternRule {
205 id: "empty_ok_return".to_string(),
206 rule_type: RuleType::Ast,
207 pattern: "return_ok_unit_with_no_logic".to_string(),
208 message: "Function returns Ok(()) with no implementation".to_string(),
209 severity: None,
210 enabled: true,
211 case_sensitive: true,
212 exclude_if: Some(ExcludeConditions {
213 attribute: Some("#[test]".to_string()),
214 in_tests: true,
215 file_patterns: None,
216 }),
217 }],
218 },
219 );
220
221 patterns.insert(
223 "architectural_violations".to_string(),
224 PatternCategory {
225 severity: Severity::Warning,
226 enabled: true,
227 rules: vec?(\.rust/)[^"']*["\']"#.to_string(),
232 message: "Hardcoded path found - use configuration instead".to_string(),
233 severity: None,
234 enabled: true,
235 case_sensitive: true,
236 exclude_if: Some(ExcludeConditions {
237 attribute: None,
238 in_tests: true,
239 file_patterns: Some(vec![
240 "**/tests/**".to_string(),
241 "**/examples/**".to_string(),
242 ]),
243 }),
244 },
245 PatternRule {
246 id: "architectural_header_missing".to_string(),
247 rule_type: RuleType::Regex,
248 pattern: r"//!\s*(?:.*\n)*?\s*//!\s*Architecture:".to_string(),
249 message: "File missing architectural principle header".to_string(),
250 severity: Some(Severity::Info),
251 enabled: false, case_sensitive: false,
253 exclude_if: Some(ExcludeConditions {
254 attribute: None,
255 in_tests: true,
256 file_patterns: Some(vec![
257 "**/tests/**".to_string(),
258 "**/benches/**".to_string(),
259 "**/examples/**".to_string(),
260 ]),
261 }),
262 },
263 ],
264 },
265 );
266
267 patterns
268 }
269
270 pub fn validate(&self) -> GuardianResult<()> {
272 if !["1.0"].contains(&self.version.as_str()) {
274 return Err(GuardianError::config(format!(
275 "Unsupported configuration version: {}. Supported versions: 1.0",
276 self.version
277 )));
278 }
279
280 for (category_name, category) in &self.patterns {
282 for rule in &category.rules {
283 let duplicate_count = category.rules.iter().filter(|r| r.id == rule.id).count();
285 if duplicate_count > 1 {
286 return Err(GuardianError::config(format!(
287 "Duplicate rule ID '{}' in category '{}'",
288 rule.id, category_name
289 )));
290 }
291
292 if matches!(rule.rule_type, RuleType::Regex) {
294 if rule.case_sensitive {
295 regex::Regex::new(&rule.pattern)
296 } else {
297 regex::RegexBuilder::new(&rule.pattern)
298 .case_insensitive(true)
299 .build()
300 }
301 .map_err(|e| {
302 GuardianError::config(format!(
303 "Invalid regex pattern in rule '{}': {}",
304 rule.id, e
305 ))
306 })?;
307 }
308 }
309 }
310
311 Ok(())
312 }
313
314 pub fn enabled_rules(&self) -> impl Iterator<Item = (&String, &PatternCategory, &PatternRule)> {
316 self.patterns
317 .iter()
318 .filter(|(_, category)| category.enabled)
319 .flat_map(|(name, category)| {
320 category
321 .rules
322 .iter()
323 .filter(|rule| rule.enabled)
324 .map(move |rule| (name, category, rule))
325 })
326 }
327
328 pub fn effective_severity(&self, category: &PatternCategory, rule: &PatternRule) -> Severity {
330 rule.severity.unwrap_or(category.severity)
331 }
332
333 pub fn to_json(&self) -> GuardianResult<String> {
335 serde_json::to_string_pretty(self)
336 .map_err(|e| GuardianError::config(format!("Failed to serialize config: {e}")))
337 }
338
339 pub fn fingerprint(&self) -> String {
341 use std::collections::hash_map::DefaultHasher;
342 use std::hash::{Hash, Hasher};
343
344 let mut hasher = DefaultHasher::new();
345
346 let mut sorted_patterns: Vec<_> = self.patterns.iter().collect();
349 sorted_patterns.sort_by_key(|(name, _)| name.as_str());
350
351 self.version.hash(&mut hasher);
353 self.paths.patterns.len().hash(&mut hasher);
354 for pattern in &self.paths.patterns {
355 pattern.hash(&mut hasher);
356 }
357 self.paths.ignore_file.hash(&mut hasher);
358
359 for (category_name, category) in sorted_patterns {
361 category_name.hash(&mut hasher);
362 category.severity.hash(&mut hasher);
363 category.enabled.hash(&mut hasher);
364
365 let mut sorted_rules = category.rules.clone();
367 sorted_rules.sort_by_key(|rule| rule.id.clone());
368
369 for rule in sorted_rules {
370 rule.id.hash(&mut hasher);
371 rule.pattern.hash(&mut hasher);
372 rule.message.hash(&mut hasher);
373 rule.enabled.hash(&mut hasher);
374 rule.case_sensitive.hash(&mut hasher);
375 }
376 }
377
378 format!("{:x}", hasher.finish())
379 }
380}
381
382impl Default for GuardianConfig {
383 fn default() -> Self {
384 Self::with_defaults()
385 }
386}
387
388fn default_true() -> bool {
389 true
390}
391
392fn build_development_marker_pattern() -> String {
394 r"\b(TODO|FIXME|HACK|XXX|BUG|REFACTOR)\b".to_string()
396}
397
398fn build_temporary_marker_pattern() -> String {
400 r"(?i)\b(for now|temporary|placeholder|stub|dummy|fake)\b".to_string()
402}
403
404fn build_unfinished_macro_pattern() -> String {
406 "macro_call:unimplemented|todo|panic".to_string()
408}
409
410pub struct ConfigBuilder {
412 config: GuardianConfig,
413}
414
415impl ConfigBuilder {
416 pub fn new() -> Self {
418 Self {
419 config: GuardianConfig::default(),
420 }
421 }
422
423 pub fn add_path_pattern(mut self, pattern: impl Into<String>) -> Self {
425 self.config.paths.patterns.push(pattern.into());
426 self
427 }
428
429 pub fn ignore_file(mut self, filename: impl Into<String>) -> Self {
431 self.config.paths.ignore_file = Some(filename.into());
432 self
433 }
434
435 pub fn add_category(mut self, name: impl Into<String>, category: PatternCategory) -> Self {
437 self.config.patterns.insert(name.into(), category);
438 self
439 }
440
441 pub fn build(self) -> GuardianResult<GuardianConfig> {
443 self.config.validate()?;
444 Ok(self.config)
445 }
446}
447
448impl Default for ConfigBuilder {
449 fn default() -> Self {
450 Self::new()
451 }
452}
453
454impl GuardianConfig {
455 pub fn verify_config_integrity(&self) -> GuardianResult<()> {
458 if self.version != "1.0" {
460 return Err(GuardianError::config(
461 "Configuration system requires version 1.0".to_string(),
462 ));
463 }
464
465 if !self.patterns.contains_key("placeholders") {
467 return Err(GuardianError::config(
468 "Core pattern category 'placeholders' missing from guardian".to_string(),
469 ));
470 }
471
472 let fingerprint1 = self.fingerprint();
474 let fingerprint2 = self.fingerprint();
475 if fingerprint1 != fingerprint2 {
476 return Err(GuardianError::config(
477 "Configuration system fingerprint inconsistent".to_string(),
478 ));
479 }
480
481 Ok(())
482 }
483
484 pub fn verify_evolution_capability() -> GuardianResult<()> {
486 let evolved_config = ConfigBuilder::new()
487 .add_path_pattern("guardian/evolution/**")
488 .ignore_file(".guardian_ignore")
489 .build()?;
490
491 if !evolved_config
492 .paths
493 .patterns
494 .contains(&"guardian/evolution/**".to_string())
495 {
496 return Err(GuardianError::config(
497 "Configuration evolution failed to integrate new patterns".to_string(),
498 ));
499 }
500
501 Ok(())
502 }
503
504 pub fn verify_serialization_fidelity(&self) -> GuardianResult<()> {
506 let yaml = serde_yaml::to_string(self).map_err(|e| {
507 GuardianError::config(format!("Configuration serialization failed: {e}"))
508 })?;
509
510 let rehydrated: Self = serde_yaml::from_str(&yaml)
511 .map_err(|e| GuardianError::config(format!("Configuration rehydration failed: {e}")))?;
512
513 if self.version != rehydrated.version {
514 return Err(GuardianError::config(
515 "Configuration version lost during serialization".to_string(),
516 ));
517 }
518
519 Ok(())
520 }
521}