Skip to main content

datasynth_eval/coherence/
audit.rs

1//! Audit evaluator.
2//!
3//! Validates audit data coherence including evidence-to-finding mapping,
4//! risk-to-procedure mapping, workpaper completeness, and materiality hierarchy.
5
6use crate::error::EvalResult;
7use serde::{Deserialize, Serialize};
8
9/// Thresholds for audit evaluation.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct AuditThresholds {
12    /// Minimum evidence-to-finding mapping rate.
13    pub min_evidence_mapping: f64,
14    /// Minimum risk-to-procedure mapping rate.
15    pub min_risk_procedure_mapping: f64,
16    /// Minimum workpaper completeness rate.
17    pub min_workpaper_completeness: f64,
18}
19
20impl Default for AuditThresholds {
21    fn default() -> Self {
22        Self {
23            min_evidence_mapping: 0.90,
24            min_risk_procedure_mapping: 0.90,
25            min_workpaper_completeness: 0.85,
26        }
27    }
28}
29
30/// Audit finding data.
31#[derive(Debug, Clone)]
32pub struct AuditFindingData {
33    /// Finding identifier.
34    pub finding_id: String,
35    /// Whether this finding has supporting evidence.
36    pub has_evidence: bool,
37    /// Number of evidence items.
38    pub evidence_count: usize,
39}
40
41/// Audit risk data.
42#[derive(Debug, Clone)]
43pub struct AuditRiskData {
44    /// Risk identifier.
45    pub risk_id: String,
46    /// Whether responsive audit procedures exist.
47    pub has_procedures: bool,
48    /// Number of responsive procedures.
49    pub procedure_count: usize,
50}
51
52/// Workpaper data.
53#[derive(Debug, Clone)]
54pub struct WorkpaperData {
55    /// Workpaper identifier.
56    pub workpaper_id: String,
57    /// Whether the workpaper has a conclusion.
58    pub has_conclusion: bool,
59    /// Whether the workpaper has references.
60    pub has_references: bool,
61    /// Whether the workpaper has a preparer.
62    pub has_preparer: bool,
63    /// Whether the workpaper has been reviewed.
64    pub has_reviewer: bool,
65}
66
67/// Materiality data.
68#[derive(Debug, Clone)]
69pub struct MaterialityData {
70    /// Overall materiality.
71    pub overall_materiality: f64,
72    /// Performance materiality.
73    pub performance_materiality: f64,
74    /// Clearly trivial threshold.
75    pub clearly_trivial: f64,
76}
77
78/// Results of audit evaluation.
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct AuditEvaluation {
81    /// Evidence-to-finding rate: fraction of findings with evidence.
82    pub evidence_to_finding_rate: f64,
83    /// Risk-to-procedure rate: fraction of risks with procedures.
84    pub risk_to_procedure_rate: f64,
85    /// Workpaper completeness: fraction with conclusion + references.
86    pub workpaper_completeness: f64,
87    /// Whether materiality hierarchy is valid (overall > performance > trivial).
88    pub materiality_hierarchy_valid: bool,
89    /// Total findings evaluated.
90    pub total_findings: usize,
91    /// Total risks evaluated.
92    pub total_risks: usize,
93    /// Total workpapers evaluated.
94    pub total_workpapers: usize,
95    /// Overall pass/fail.
96    pub passes: bool,
97    /// Issues found.
98    pub issues: Vec<String>,
99}
100
101/// Evaluator for audit coherence.
102pub struct AuditEvaluator {
103    thresholds: AuditThresholds,
104}
105
106impl AuditEvaluator {
107    /// Create a new evaluator with default thresholds.
108    pub fn new() -> Self {
109        Self {
110            thresholds: AuditThresholds::default(),
111        }
112    }
113
114    /// Create with custom thresholds.
115    pub fn with_thresholds(thresholds: AuditThresholds) -> Self {
116        Self { thresholds }
117    }
118
119    /// Evaluate audit data.
120    pub fn evaluate(
121        &self,
122        findings: &[AuditFindingData],
123        risks: &[AuditRiskData],
124        workpapers: &[WorkpaperData],
125        materiality: &Option<MaterialityData>,
126    ) -> EvalResult<AuditEvaluation> {
127        let mut issues = Vec::new();
128
129        // 1. Evidence-to-finding mapping
130        let findings_with_evidence = findings.iter().filter(|f| f.has_evidence).count();
131        let evidence_to_finding_rate = if findings.is_empty() {
132            1.0
133        } else {
134            findings_with_evidence as f64 / findings.len() as f64
135        };
136
137        // 2. Risk-to-procedure mapping
138        let risks_with_procedures = risks.iter().filter(|r| r.has_procedures).count();
139        let risk_to_procedure_rate = if risks.is_empty() {
140            1.0
141        } else {
142            risks_with_procedures as f64 / risks.len() as f64
143        };
144
145        // 3. Workpaper completeness (has conclusion AND references)
146        let complete_workpapers = workpapers
147            .iter()
148            .filter(|w| w.has_conclusion && w.has_references)
149            .count();
150        let workpaper_completeness = if workpapers.is_empty() {
151            1.0
152        } else {
153            complete_workpapers as f64 / workpapers.len() as f64
154        };
155
156        // 4. Materiality hierarchy
157        let materiality_hierarchy_valid = if let Some(ref mat) = materiality {
158            mat.overall_materiality > mat.performance_materiality
159                && mat.performance_materiality > mat.clearly_trivial
160                && mat.clearly_trivial >= 0.0
161        } else {
162            true // Not provided = not checked
163        };
164
165        // Check thresholds
166        if evidence_to_finding_rate < self.thresholds.min_evidence_mapping {
167            issues.push(format!(
168                "Evidence-to-finding rate {:.3} < {:.3}",
169                evidence_to_finding_rate, self.thresholds.min_evidence_mapping
170            ));
171        }
172        if risk_to_procedure_rate < self.thresholds.min_risk_procedure_mapping {
173            issues.push(format!(
174                "Risk-to-procedure rate {:.3} < {:.3}",
175                risk_to_procedure_rate, self.thresholds.min_risk_procedure_mapping
176            ));
177        }
178        if workpaper_completeness < self.thresholds.min_workpaper_completeness {
179            issues.push(format!(
180                "Workpaper completeness {:.3} < {:.3}",
181                workpaper_completeness, self.thresholds.min_workpaper_completeness
182            ));
183        }
184        if !materiality_hierarchy_valid {
185            issues.push(
186                "Materiality hierarchy invalid: expected overall > performance > trivial"
187                    .to_string(),
188            );
189        }
190
191        let passes = issues.is_empty();
192
193        Ok(AuditEvaluation {
194            evidence_to_finding_rate,
195            risk_to_procedure_rate,
196            workpaper_completeness,
197            materiality_hierarchy_valid,
198            total_findings: findings.len(),
199            total_risks: risks.len(),
200            total_workpapers: workpapers.len(),
201            passes,
202            issues,
203        })
204    }
205}
206
207impl Default for AuditEvaluator {
208    fn default() -> Self {
209        Self::new()
210    }
211}
212
213#[cfg(test)]
214#[allow(clippy::unwrap_used)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn test_valid_audit() {
220        let evaluator = AuditEvaluator::new();
221        let findings = vec![AuditFindingData {
222            finding_id: "F001".to_string(),
223            has_evidence: true,
224            evidence_count: 3,
225        }];
226        let risks = vec![AuditRiskData {
227            risk_id: "R001".to_string(),
228            has_procedures: true,
229            procedure_count: 2,
230        }];
231        let workpapers = vec![WorkpaperData {
232            workpaper_id: "WP001".to_string(),
233            has_conclusion: true,
234            has_references: true,
235            has_preparer: true,
236            has_reviewer: true,
237        }];
238        let materiality = Some(MaterialityData {
239            overall_materiality: 100_000.0,
240            performance_materiality: 75_000.0,
241            clearly_trivial: 5_000.0,
242        });
243
244        let result = evaluator
245            .evaluate(&findings, &risks, &workpapers, &materiality)
246            .unwrap();
247        assert!(result.passes);
248        assert!(result.materiality_hierarchy_valid);
249    }
250
251    #[test]
252    fn test_missing_evidence() {
253        let evaluator = AuditEvaluator::new();
254        let findings = vec![
255            AuditFindingData {
256                finding_id: "F001".to_string(),
257                has_evidence: false,
258                evidence_count: 0,
259            },
260            AuditFindingData {
261                finding_id: "F002".to_string(),
262                has_evidence: false,
263                evidence_count: 0,
264            },
265        ];
266
267        let result = evaluator.evaluate(&findings, &[], &[], &None).unwrap();
268        assert!(!result.passes);
269        assert_eq!(result.evidence_to_finding_rate, 0.0);
270    }
271
272    #[test]
273    fn test_invalid_materiality() {
274        let evaluator = AuditEvaluator::new();
275        let materiality = Some(MaterialityData {
276            overall_materiality: 50_000.0,
277            performance_materiality: 100_000.0, // Higher than overall!
278            clearly_trivial: 5_000.0,
279        });
280
281        let result = evaluator.evaluate(&[], &[], &[], &materiality).unwrap();
282        assert!(!result.materiality_hierarchy_valid);
283        assert!(!result.passes);
284    }
285
286    #[test]
287    fn test_empty_data() {
288        let evaluator = AuditEvaluator::new();
289        let result = evaluator.evaluate(&[], &[], &[], &None).unwrap();
290        assert!(result.passes);
291    }
292}