ringkernel_accnet/kernels/
analysis.rs1use crate::models::{
9 AccountType, AccountingNetwork, FraudPattern, FraudPatternType, GaapViolation,
10 GaapViolationType,
11};
12
13#[derive(Debug, Clone)]
15pub struct AnalysisConfig {
16 pub block_size: u32,
18 pub suspense_threshold: f32,
20 pub benford_enabled: bool,
22 pub benford_chi_sq_threshold: f64,
24 pub circular_detection_enabled: bool,
26 pub max_cycle_length: usize,
28}
29
30impl Default for AnalysisConfig {
31 fn default() -> Self {
32 Self {
33 block_size: 256,
34 suspense_threshold: 0.7,
35 benford_enabled: true,
36 benford_chi_sq_threshold: 15.507, circular_detection_enabled: true,
38 max_cycle_length: 10,
39 }
40 }
41}
42
43#[derive(Debug, Clone, Default)]
45pub struct AnalysisResult {
46 pub suspense_accounts: Vec<(u16, f32)>,
48 pub gaap_violations: Vec<GaapViolation>,
50 pub fraud_patterns: Vec<FraudPattern>,
52 pub stats: AnalysisStats,
54}
55
56#[derive(Debug, Clone, Default)]
58pub struct AnalysisStats {
59 pub accounts_analyzed: usize,
61 pub flows_analyzed: usize,
63 pub suspense_count: usize,
65 pub gaap_violation_count: usize,
67 pub fraud_pattern_count: usize,
69}
70
71pub struct AnalysisKernel {
73 config: AnalysisConfig,
74}
75
76impl AnalysisKernel {
77 pub fn new(config: AnalysisConfig) -> Self {
79 Self { config }
80 }
81
82 pub fn analyze(&self, network: &AccountingNetwork) -> AnalysisResult {
84 let mut result = AnalysisResult::default();
85
86 for account in &network.accounts {
88 if account.suspense_score >= self.config.suspense_threshold {
89 result
90 .suspense_accounts
91 .push((account.index, account.suspense_score));
92 }
93 }
94
95 result.gaap_violations = self.detect_gaap_violations(network);
97
98 if self.config.benford_enabled {
100 if let Some(pattern) = self.check_benford_violation(network) {
101 result.fraud_patterns.push(pattern);
102 }
103 }
104
105 if self.config.circular_detection_enabled {
106 result
107 .fraud_patterns
108 .extend(self.detect_circular_flows(network));
109 }
110
111 result.stats.accounts_analyzed = network.accounts.len();
113 result.stats.flows_analyzed = network.flows.len();
114 result.stats.suspense_count = result.suspense_accounts.len();
115 result.stats.gaap_violation_count = result.gaap_violations.len();
116 result.stats.fraud_pattern_count = result.fraud_patterns.len();
117
118 result
119 }
120
121 fn detect_gaap_violations(&self, network: &AccountingNetwork) -> Vec<GaapViolation> {
123 let mut violations = Vec::new();
124
125 for flow in &network.flows {
127 let source = network.accounts.get(flow.source_account_index as usize);
128 let target = network.accounts.get(flow.target_account_index as usize);
129
130 if let (Some(src), Some(tgt)) = (source, target) {
131 if src.account_type == AccountType::Revenue
133 && tgt.account_type == AccountType::Asset
134 {
135 violations.push(GaapViolation::new(
136 GaapViolationType::RevenueToCashDirect,
137 flow.source_account_index,
138 flow.target_account_index,
139 flow.amount,
140 flow.journal_entry_id,
141 ));
142 }
143
144 if src.account_type == AccountType::Revenue
146 && tgt.account_type == AccountType::Expense
147 {
148 violations.push(GaapViolation::new(
149 GaapViolationType::RevenueToExpense,
150 flow.source_account_index,
151 flow.target_account_index,
152 flow.amount,
153 flow.journal_entry_id,
154 ));
155 }
156 }
157 }
158
159 violations
160 }
161
162 fn check_benford_violation(&self, network: &AccountingNetwork) -> Option<FraudPattern> {
164 let mut digit_counts = [0u32; 9];
166 let mut total = 0u32;
167
168 for flow in &network.flows {
169 let amount = flow.amount.abs();
170 if amount.mantissa > 0 {
171 let first_digit = Self::first_digit(amount.mantissa.unsigned_abs());
172 if (1..=9).contains(&first_digit) {
173 digit_counts[(first_digit - 1) as usize] += 1;
174 total += 1;
175 }
176 }
177 }
178
179 if total < 100 {
180 return None; }
182
183 let chi_sq = crate::models::benford_chi_squared(&digit_counts, total);
184
185 if chi_sq > self.config.benford_chi_sq_threshold {
186 let mut pattern = FraudPattern::new(FraudPatternType::BenfordViolation);
187 pattern.risk_score = (chi_sq / 50.0).min(1.0) as f32;
188 Some(pattern)
189 } else {
190 None
191 }
192 }
193
194 fn first_digit(mut n: u128) -> u32 {
196 while n >= 10 {
197 n /= 10;
198 }
199 n as u32
200 }
201
202 fn detect_circular_flows(&self, network: &AccountingNetwork) -> Vec<FraudPattern> {
204 let mut patterns = Vec::new();
205 let n = network.accounts.len();
206
207 if n == 0 {
208 return patterns;
209 }
210
211 let mut adj: Vec<Vec<usize>> = vec![vec![]; n];
213 for flow in &network.flows {
214 let src = flow.source_account_index as usize;
215 let tgt = flow.target_account_index as usize;
216 if src < n && tgt < n {
217 adj[src].push(tgt);
218 }
219 }
220
221 let mut visited = vec![false; n];
223 let mut rec_stack = vec![false; n];
224
225 for start in 0..n {
226 if !visited[start] {
227 let mut path = Vec::new();
228 if self.has_cycle(
229 &adj,
230 start,
231 &mut visited,
232 &mut rec_stack,
233 &mut path,
234 self.config.max_cycle_length,
235 ) {
236 let mut pattern = FraudPattern::new(FraudPatternType::CircularFlow);
237 pattern.account_count = path.len() as u16;
238 for (i, &idx) in path.iter().enumerate().take(8) {
239 pattern.involved_accounts[i] = idx as u16;
240 }
241 patterns.push(pattern);
242 }
243 }
244 }
245
246 patterns
247 }
248
249 fn has_cycle(
251 &self,
252 adj: &[Vec<usize>],
253 node: usize,
254 visited: &mut [bool],
255 rec_stack: &mut [bool],
256 path: &mut Vec<usize>,
257 max_len: usize,
258 ) -> bool {
259 visited[node] = true;
260 rec_stack[node] = true;
261 path.push(node);
262
263 if path.len() > max_len {
264 path.pop();
265 rec_stack[node] = false;
266 return false;
267 }
268
269 for &neighbor in &adj[node] {
270 if !visited[neighbor] {
271 if self.has_cycle(adj, neighbor, visited, rec_stack, path, max_len) {
272 return true;
273 }
274 } else if rec_stack[neighbor] {
275 return true;
276 }
277 }
278
279 path.pop();
280 rec_stack[node] = false;
281 false
282 }
283}
284
285impl Default for AnalysisKernel {
286 fn default() -> Self {
287 Self::new(AnalysisConfig::default())
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn test_analysis_kernel_creation() {
297 let kernel = AnalysisKernel::default();
298 assert!(kernel.config.benford_enabled);
299 }
300
301 #[test]
302 fn test_first_digit() {
303 assert_eq!(AnalysisKernel::first_digit(12345), 1);
304 assert_eq!(AnalysisKernel::first_digit(999), 9);
305 assert_eq!(AnalysisKernel::first_digit(5), 5);
306 }
307}