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
11impl AuditEngine {
12    /// Judge every DiffEntry against the AuditDefinition.
13    pub fn evaluate(diffs: &[DiffEntry], definition: &AuditDefinition) -> AuditResult {
14        let mut results = Vec::new();
15
16        for diff in diffs {
17            let result = judge(diff, definition);
18            results.push(result);
19        }
20
21        AuditResult::new(results)
22    }
23}
24
25fn judge(diff: &DiffEntry, definition: &AuditDefinition) -> FileAuditResult {
26    // Diff-level errors (Unreadable / Incomparable) → always Error.
27    if diff.diff_type.is_error() {
28        return FileAuditResult {
29            diff: diff.clone(),
30            entry: None,
31            status: AuditStatus::Error,
32            detail: diff.error_detail.clone().or_else(|| {
33                Some("File could not be read or compared.".into())
34            }),
35            warnings: Vec::new(),
36        };
37    }
38
39    // Unchanged entries have no diff to audit — auto-OK regardless of rules.
40    if diff.diff_type == DiffType::Unchanged {
41        return FileAuditResult {
42            diff: diff.clone(),
43            entry: definition.find_entry(&diff.path).cloned(),
44            status: AuditStatus::Ok,
45            detail: None,
46            warnings: Vec::new(),
47        };
48    }
49
50    // Look up the matching entry.
51    let entry = match definition.find_entry(&diff.path) {
52        Some(e) => e,
53        None => {
54            return FileAuditResult {
55                diff: diff.clone(),
56                entry: None,
57                status: AuditStatus::Pending,
58                detail: Some("No audit rule defined for this path.".into()),
59                warnings: Vec::new(),
60            };
61        }
62    };
63
64    // Disabled entries → Ignored.
65    if !entry.enabled {
66        return FileAuditResult {
67            diff: diff.clone(),
68            entry: Some(entry.clone()),
69            status: AuditStatus::Ignored,
70            detail: Some("Entry is disabled.".into()),
71            warnings: Vec::new(),
72        };
73    }
74
75    // Empty reason → treat as Pending (not yet human-approved).
76    if entry.reason.trim().is_empty() {
77        return FileAuditResult {
78            diff: diff.clone(),
79            entry: Some(entry.clone()),
80            status: AuditStatus::Pending,
81            detail: Some("Entry exists but has no reason — human approval required.".into()),
82            warnings: Vec::new(),
83        };
84    }
85
86    // Diff-type mismatch → Failed.
87    if entry.diff_type != diff.diff_type {
88        return FileAuditResult {
89            diff: diff.clone(),
90            entry: Some(entry.clone()),
91            status: AuditStatus::Failed,
92            detail: Some(format!(
93                "Expected diff type {:?} but found {:?}.",
94                entry.diff_type, diff.diff_type
95            )),
96            warnings: Vec::new(),
97        };
98    }
99
100    // Content strategy check.
101    match strategy::evaluate(&entry.strategy, diff) {
102        Ok(()) => {
103            let warns = warning::collect(diff, entry);
104            FileAuditResult {
105                diff: diff.clone(),
106                entry: Some(entry.clone()),
107                status: AuditStatus::Ok,
108                detail: None,
109                warnings: warns,
110            }
111        }
112        Err(msg) => FileAuditResult {
113            diff: diff.clone(),
114            entry: Some(entry.clone()),
115            status: AuditStatus::Failed,
116            detail: Some(msg),
117            warnings: Vec::new(),
118        },
119    }
120}