Skip to main content

aaai_core/audit/
engine.rs

1//! Audit engine: matches DiffEntries against AuditDefinition → AuditResult.
2
3use crate::config::definition::AuditDefinition;
4use crate::diff::entry::{DiffEntry, DiffType};
5use super::result::{AuditResult, AuditStatus, FileAuditResult};
6use super::strategy;
7
8pub struct AuditEngine;
9
10impl AuditEngine {
11    /// Judge every DiffEntry against the AuditDefinition.
12    pub fn evaluate(diffs: &[DiffEntry], definition: &AuditDefinition) -> AuditResult {
13        let mut results = Vec::new();
14
15        for diff in diffs {
16            let result = judge(diff, definition);
17            results.push(result);
18        }
19
20        AuditResult::new(results)
21    }
22}
23
24fn judge(diff: &DiffEntry, definition: &AuditDefinition) -> FileAuditResult {
25    // Diff-level errors (Unreadable / Incomparable) → always Error.
26    if diff.diff_type.is_error() {
27        return FileAuditResult {
28            diff: diff.clone(),
29            entry: None,
30            status: AuditStatus::Error,
31            detail: diff.error_detail.clone().or_else(|| {
32                Some("File could not be read or compared.".into())
33            }),
34        };
35    }
36
37    // Unchanged entries have no diff to audit — auto-OK regardless of rules.
38    if diff.diff_type == DiffType::Unchanged {
39        return FileAuditResult {
40            diff: diff.clone(),
41            entry: definition.find_entry(&diff.path).cloned(),
42            status: AuditStatus::Ok,
43            detail: None,
44        };
45    }
46
47    // Look up the matching entry.
48    let entry = match definition.find_entry(&diff.path) {
49        Some(e) => e,
50        None => {
51            return FileAuditResult {
52                diff: diff.clone(),
53                entry: None,
54                status: AuditStatus::Pending,
55                detail: Some("No audit rule defined for this path.".into()),
56            };
57        }
58    };
59
60    // Disabled entries → Ignored.
61    if !entry.enabled {
62        return FileAuditResult {
63            diff: diff.clone(),
64            entry: Some(entry.clone()),
65            status: AuditStatus::Ignored,
66            detail: Some("Entry is disabled.".into()),
67        };
68    }
69
70    // Diff-type mismatch → Failed.
71    if entry.diff_type != diff.diff_type {
72        return FileAuditResult {
73            diff: diff.clone(),
74            entry: Some(entry.clone()),
75            status: AuditStatus::Failed,
76            detail: Some(format!(
77                "Expected diff type {:?} but found {:?}.",
78                entry.diff_type, diff.diff_type
79            )),
80        };
81    }
82
83    // Content strategy check.
84    match strategy::evaluate(&entry.strategy, diff) {
85        Ok(()) => FileAuditResult {
86            diff: diff.clone(),
87            entry: Some(entry.clone()),
88            status: AuditStatus::Ok,
89            detail: None,
90        },
91        Err(msg) => FileAuditResult {
92            diff: diff.clone(),
93            entry: Some(entry.clone()),
94            status: AuditStatus::Failed,
95            detail: Some(msg),
96        },
97    }
98}