Skip to main content

aaai_core/audit/
engine.rs

1//! Audit engine: matches DiffEntries against AuditDefinition → AuditResult.
2
3use crate::audit::warning;
4use crate::config::definition::AuditDefinition;
5use crate::diff::entry::{DiffEntry, DiffType};
6use super::result::{AuditResult, AuditStatus, FileAuditResult};
7use super::strategy;
8
9pub struct AuditEngine;
10
11/// Options for audit evaluation.
12#[derive(Debug, Clone, Default)]
13pub struct AuditOptions {
14    /// Warning kind IDs to suppress (e.g. ["no-approver"]).
15    pub suppress_warnings: Vec<String>,
16}
17
18impl AuditEngine {
19    /// Judge every DiffEntry against the AuditDefinition.
20    pub fn evaluate(diffs: &[DiffEntry], definition: &AuditDefinition) -> AuditResult {
21        Self::evaluate_with_options(diffs, definition, &AuditOptions::default())
22    }
23
24    /// Evaluate with custom options (warning suppression, etc.)
25    pub fn evaluate_with_options(
26        diffs: &[DiffEntry],
27        definition: &AuditDefinition,
28        options: &AuditOptions,
29    ) -> AuditResult {
30        let mut results = Vec::new();
31
32        for diff in diffs {
33            let mut result = judge(diff, definition);
34            // Filter suppressed warnings.
35            if !options.suppress_warnings.is_empty() {
36                result.warnings.retain(|w| {
37                    !options.suppress_warnings.iter().any(|s| s == w.kind())
38                });
39            }
40            results.push(result);
41        }
42
43        AuditResult::new(results)
44    }
45}
46
47fn judge(diff: &DiffEntry, definition: &AuditDefinition) -> FileAuditResult {
48    // Diff-level errors (Unreadable / Incomparable) → always Error.
49    if diff.diff_type.is_error() {
50        return FileAuditResult {
51            diff: diff.clone(),
52            entry: None,
53            status: AuditStatus::Error,
54            detail: diff.error_detail.clone().or_else(|| {
55                Some("File could not be read or compared.".into())
56            }),
57            warnings: Vec::new(),
58        };
59    }
60
61    // Unchanged entries have no diff to audit — auto-OK regardless of rules.
62    if diff.diff_type == DiffType::Unchanged {
63        return FileAuditResult {
64            diff: diff.clone(),
65            entry: definition.find_entry(&diff.path).cloned(),
66            status: AuditStatus::Ok,
67            detail: None,
68            warnings: Vec::new(),
69        };
70    }
71
72    // Look up the matching entry.
73    let entry = match definition.find_entry(&diff.path) {
74        Some(e) => e,
75        None => {
76            return FileAuditResult {
77                diff: diff.clone(),
78                entry: None,
79                status: AuditStatus::Pending,
80                detail: Some("No audit rule defined for this path.".into()),
81                warnings: Vec::new(),
82            };
83        }
84    };
85
86    // Disabled entries → Ignored.
87    if !entry.enabled {
88        return FileAuditResult {
89            diff: diff.clone(),
90            entry: Some(entry.clone()),
91            status: AuditStatus::Ignored,
92            detail: Some("Entry is disabled.".into()),
93            warnings: Vec::new(),
94        };
95    }
96
97    // Empty reason → treat as Pending (not yet human-approved).
98    if entry.reason.trim().is_empty() {
99        return FileAuditResult {
100            diff: diff.clone(),
101            entry: Some(entry.clone()),
102            status: AuditStatus::Pending,
103            detail: Some("Entry exists but has no reason — human approval required.".into()),
104            warnings: Vec::new(),
105        };
106    }
107
108    // Diff-type mismatch → Failed.
109    if entry.diff_type != diff.diff_type {
110        return FileAuditResult {
111            diff: diff.clone(),
112            entry: Some(entry.clone()),
113            status: AuditStatus::Failed,
114            detail: Some(format!(
115                "Expected diff type {:?} but found {:?}.",
116                entry.diff_type, diff.diff_type
117            )),
118            warnings: Vec::new(),
119        };
120    }
121
122    // Content strategy check.
123    match strategy::evaluate(&entry.strategy, diff) {
124        Ok(()) => {
125            let warns = warning::collect(diff, entry);
126            FileAuditResult {
127                diff: diff.clone(),
128                entry: Some(entry.clone()),
129                status: AuditStatus::Ok,
130                detail: None,
131                warnings: warns,
132            }
133        }
134        Err(msg) => FileAuditResult {
135            diff: diff.clone(),
136            entry: Some(entry.clone()),
137            status: AuditStatus::Failed,
138            detail: Some(msg),
139            warnings: Vec::new(),
140        },
141    }
142}