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)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn test_valid_audit() {
219        let evaluator = AuditEvaluator::new();
220        let findings = vec![AuditFindingData {
221            finding_id: "F001".to_string(),
222            has_evidence: true,
223            evidence_count: 3,
224        }];
225        let risks = vec![AuditRiskData {
226            risk_id: "R001".to_string(),
227            has_procedures: true,
228            procedure_count: 2,
229        }];
230        let workpapers = vec![WorkpaperData {
231            workpaper_id: "WP001".to_string(),
232            has_conclusion: true,
233            has_references: true,
234            has_preparer: true,
235            has_reviewer: true,
236        }];
237        let materiality = Some(MaterialityData {
238            overall_materiality: 100_000.0,
239            performance_materiality: 75_000.0,
240            clearly_trivial: 5_000.0,
241        });
242
243        let result = evaluator
244            .evaluate(&findings, &risks, &workpapers, &materiality)
245            .unwrap();
246        assert!(result.passes);
247        assert!(result.materiality_hierarchy_valid);
248    }
249
250    #[test]
251    fn test_missing_evidence() {
252        let evaluator = AuditEvaluator::new();
253        let findings = vec![
254            AuditFindingData {
255                finding_id: "F001".to_string(),
256                has_evidence: false,
257                evidence_count: 0,
258            },
259            AuditFindingData {
260                finding_id: "F002".to_string(),
261                has_evidence: false,
262                evidence_count: 0,
263            },
264        ];
265
266        let result = evaluator.evaluate(&findings, &[], &[], &None).unwrap();
267        assert!(!result.passes);
268        assert_eq!(result.evidence_to_finding_rate, 0.0);
269    }
270
271    #[test]
272    fn test_invalid_materiality() {
273        let evaluator = AuditEvaluator::new();
274        let materiality = Some(MaterialityData {
275            overall_materiality: 50_000.0,
276            performance_materiality: 100_000.0, // Higher than overall!
277            clearly_trivial: 5_000.0,
278        });
279
280        let result = evaluator.evaluate(&[], &[], &[], &materiality).unwrap();
281        assert!(!result.materiality_hierarchy_valid);
282        assert!(!result.passes);
283    }
284
285    #[test]
286    fn test_empty_data() {
287        let evaluator = AuditEvaluator::new();
288        let result = evaluator.evaluate(&[], &[], &[], &None).unwrap();
289        assert!(result.passes);
290    }
291}