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)]
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, 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}