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).case_insensitive(true).build()
298 }
299 .map_err(|e| {
300 GuardianError::config(format!(
301 "Invalid regex pattern in rule '{}': {}",
302 rule.id, e
303 ))
304 })?;
305 }
306 }
307 }
308
309 Ok(())
310 }
311
312 pub fn enabled_rules(&self) -> impl Iterator<Item = (&String, &PatternCategory, &PatternRule)> {
314 self.patterns.iter().filter(|(_, category)| category.enabled).flat_map(
315 |(name, category)| {
316 category
317 .rules
318 .iter()
319 .filter(|rule| rule.enabled)
320 .map(move |rule| (name, category, rule))
321 },
322 )
323 }
324
325 pub fn effective_severity(&self, category: &PatternCategory, rule: &PatternRule) -> Severity {
327 rule.severity.unwrap_or(category.severity)
328 }
329
330 pub fn to_json(&self) -> GuardianResult<String> {
332 serde_json::to_string_pretty(self)
333 .map_err(|e| GuardianError::config(format!("Failed to serialize config: {e}")))
334 }
335
336 pub fn fingerprint(&self) -> String {
338 use std::collections::hash_map::DefaultHasher;
339 use std::hash::{Hash, Hasher};
340
341 let mut hasher = DefaultHasher::new();
342
343 let mut sorted_patterns: Vec<_> = self.patterns.iter().collect();
346 sorted_patterns.sort_by_key(|(name, _)| name.as_str());
347
348 self.version.hash(&mut hasher);
350 self.paths.patterns.len().hash(&mut hasher);
351 for pattern in &self.paths.patterns {
352 pattern.hash(&mut hasher);
353 }
354 self.paths.ignore_file.hash(&mut hasher);
355
356 for (category_name, category) in sorted_patterns {
358 category_name.hash(&mut hasher);
359 category.severity.hash(&mut hasher);
360 category.enabled.hash(&mut hasher);
361
362 let mut sorted_rules = category.rules.clone();
364 sorted_rules.sort_by_key(|rule| rule.id.clone());
365
366 for rule in sorted_rules {
367 rule.id.hash(&mut hasher);
368 rule.pattern.hash(&mut hasher);
369 rule.message.hash(&mut hasher);
370 rule.enabled.hash(&mut hasher);
371 rule.case_sensitive.hash(&mut hasher);
372 }
373 }
374
375 format!("{:x}", hasher.finish())
376 }
377}
378
379impl Default for GuardianConfig {
380 fn default() -> Self {
381 Self::with_defaults()
382 }
383}
384
385fn default_true() -> bool {
386 true
387}
388
389fn build_development_marker_pattern() -> String {
391 r"\b(TODO|FIXME|HACK|XXX|BUG|REFACTOR)\b".to_string()
393}
394
395fn build_temporary_marker_pattern() -> String {
397 r"(?i)\b(for now|temporary|placeholder|stub|dummy|fake)\b".to_string()
399}
400
401fn build_unfinished_macro_pattern() -> String {
403 "macro_call:unimplemented|todo|panic".to_string()
405}
406
407pub struct ConfigBuilder {
409 config: GuardianConfig,
410}
411
412impl ConfigBuilder {
413 pub fn new() -> Self {
415 Self { config: GuardianConfig::default() }
416 }
417
418 pub fn add_path_pattern(mut self, pattern: impl Into<String>) -> Self {
420 self.config.paths.patterns.push(pattern.into());
421 self
422 }
423
424 pub fn ignore_file(mut self, filename: impl Into<String>) -> Self {
426 self.config.paths.ignore_file = Some(filename.into());
427 self
428 }
429
430 pub fn add_category(mut self, name: impl Into<String>, category: PatternCategory) -> Self {
432 self.config.patterns.insert(name.into(), category);
433 self
434 }
435
436 pub fn build(self) -> GuardianResult<GuardianConfig> {
438 self.config.validate()?;
439 Ok(self.config)
440 }
441}
442
443impl Default for ConfigBuilder {
444 fn default() -> Self {
445 Self::new()
446 }
447}
448
449impl GuardianConfig {
450 pub fn verify_config_integrity(&self) -> GuardianResult<()> {
453 if self.version != "1.0" {
455 return Err(GuardianError::config(
456 "Configuration system requires version 1.0".to_string(),
457 ));
458 }
459
460 if !self.patterns.contains_key("placeholders") {
462 return Err(GuardianError::config(
463 "Core pattern category 'placeholders' missing from guardian".to_string(),
464 ));
465 }
466
467 let fingerprint1 = self.fingerprint();
469 let fingerprint2 = self.fingerprint();
470 if fingerprint1 != fingerprint2 {
471 return Err(GuardianError::config(
472 "Configuration system fingerprint inconsistent".to_string(),
473 ));
474 }
475
476 Ok(())
477 }
478
479 pub fn verify_evolution_capability() -> GuardianResult<()> {
481 let evolved_config = ConfigBuilder::new()
482 .add_path_pattern("guardian/evolution/**")
483 .ignore_file(".guardian_ignore")
484 .build()?;
485
486 if !evolved_config.paths.patterns.contains(&"guardian/evolution/**".to_string()) {
487 return Err(GuardianError::config(
488 "Configuration evolution failed to integrate new patterns".to_string(),
489 ));
490 }
491
492 Ok(())
493 }
494
495 pub fn verify_serialization_fidelity(&self) -> GuardianResult<()> {
497 let yaml = serde_yaml::to_string(self).map_err(|e| {
498 GuardianError::config(format!("Configuration serialization failed: {e}"))
499 })?;
500
501 let rehydrated: Self = serde_yaml::from_str(&yaml)
502 .map_err(|e| GuardianError::config(format!("Configuration rehydration failed: {e}")))?;
503
504 if self.version != rehydrated.version {
505 return Err(GuardianError::config(
506 "Configuration version lost during serialization".to_string(),
507 ));
508 }
509
510 Ok(())
511 }
512}