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    // Empty reason → treat as Pending (not yet human-approved).
71    if entry.reason.trim().is_empty() {
72        return FileAuditResult {
73            diff: diff.clone(),
74            entry: Some(entry.clone()),
75            status: AuditStatus::Pending,
76            detail: Some("Entry exists but has no reason — human approval required.".into()),
77        };
78    }
79
80    // Diff-type mismatch → Failed.
81    if entry.diff_type != diff.diff_type {
82        return FileAuditResult {
83            diff: diff.clone(),
84            entry: Some(entry.clone()),
85            status: AuditStatus::Failed,
86            detail: Some(format!(
87                "Expected diff type {:?} but found {:?}.",
88                entry.diff_type, diff.diff_type
89            )),
90        };
91    }
92
93    // Content strategy check.
94    match strategy::evaluate(&entry.strategy, diff) {
95        Ok(()) => FileAuditResult {
96            diff: diff.clone(),
97            entry: Some(entry.clone()),
98            status: AuditStatus::Ok,
99            detail: None,
100        },
101        Err(msg) => FileAuditResult {
102            diff: diff.clone(),
103            entry: Some(entry.clone()),
104            status: AuditStatus::Failed,
105            detail: Some(msg),
106        },
107    }
108}