datasynth_eval/coherence/
audit.rs1use crate::error::EvalResult;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct AuditThresholds {
12 pub min_evidence_mapping: f64,
14 pub min_risk_procedure_mapping: f64,
16 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#[derive(Debug, Clone)]
32pub struct AuditFindingData {
33 pub finding_id: String,
35 pub has_evidence: bool,
37 pub evidence_count: usize,
39}
40
41#[derive(Debug, Clone)]
43pub struct AuditRiskData {
44 pub risk_id: String,
46 pub has_procedures: bool,
48 pub procedure_count: usize,
50}
51
52#[derive(Debug, Clone)]
54pub struct WorkpaperData {
55 pub workpaper_id: String,
57 pub has_conclusion: bool,
59 pub has_references: bool,
61 pub has_preparer: bool,
63 pub has_reviewer: bool,
65}
66
67#[derive(Debug, Clone)]
69pub struct MaterialityData {
70 pub overall_materiality: f64,
72 pub performance_materiality: f64,
74 pub clearly_trivial: f64,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct AuditEvaluation {
81 pub evidence_to_finding_rate: f64,
83 pub risk_to_procedure_rate: f64,
85 pub workpaper_completeness: f64,
87 pub materiality_hierarchy_valid: bool,
89 pub total_findings: usize,
91 pub total_risks: usize,
93 pub total_workpapers: usize,
95 pub passes: bool,
97 pub issues: Vec<String>,
99}
100
101pub struct AuditEvaluator {
103 thresholds: AuditThresholds,
104}
105
106impl AuditEvaluator {
107 pub fn new() -> Self {
109 Self {
110 thresholds: AuditThresholds::default(),
111 }
112 }
113
114 pub fn with_thresholds(thresholds: AuditThresholds) -> Self {
116 Self { thresholds }
117 }
118
119 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 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 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 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 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 };
164
165 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, 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}