Skip to main content

ringkernel_accnet/kernels/
analysis.rs

1//! Network analysis kernels for fraud detection and GAAP compliance.
2//!
3//! These kernels analyze the accounting network graph to detect:
4//! - Suspense accounts
5//! - GAAP violations
6//! - Fraud patterns (circular flows, Benford violations, etc.)
7
8use crate::models::{
9    AccountType, AccountingNetwork, FraudPattern, FraudPatternType, GaapViolation,
10    GaapViolationType,
11};
12
13/// Configuration for analysis kernels.
14#[derive(Debug, Clone)]
15pub struct AnalysisConfig {
16    /// Block size for GPU dispatch.
17    pub block_size: u32,
18    /// Suspense score threshold.
19    pub suspense_threshold: f32,
20    /// Enable Benford's Law analysis.
21    pub benford_enabled: bool,
22    /// Chi-squared threshold for Benford violation.
23    pub benford_chi_sq_threshold: f64,
24    /// Enable circular flow detection.
25    pub circular_detection_enabled: bool,
26    /// Maximum cycle length to detect.
27    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, // Chi-squared critical value for 8 DOF at α=0.05
37            circular_detection_enabled: true,
38            max_cycle_length: 10,
39        }
40    }
41}
42
43/// Result of network analysis.
44#[derive(Debug, Clone, Default)]
45pub struct AnalysisResult {
46    /// Detected suspense accounts (index, score).
47    pub suspense_accounts: Vec<(u16, f32)>,
48    /// Detected GAAP violations.
49    pub gaap_violations: Vec<GaapViolation>,
50    /// Detected fraud patterns.
51    pub fraud_patterns: Vec<FraudPattern>,
52    /// Analysis statistics.
53    pub stats: AnalysisStats,
54}
55
56/// Statistics from analysis.
57#[derive(Debug, Clone, Default)]
58pub struct AnalysisStats {
59    /// Accounts analyzed.
60    pub accounts_analyzed: usize,
61    /// Flows analyzed.
62    pub flows_analyzed: usize,
63    /// Suspense accounts found.
64    pub suspense_count: usize,
65    /// GAAP violations found.
66    pub gaap_violation_count: usize,
67    /// Fraud patterns found.
68    pub fraud_pattern_count: usize,
69}
70
71/// Network analysis kernel dispatcher.
72pub struct AnalysisKernel {
73    config: AnalysisConfig,
74}
75
76impl AnalysisKernel {
77    /// Create a new analysis kernel.
78    pub fn new(config: AnalysisConfig) -> Self {
79        Self { config }
80    }
81
82    /// Analyze the network (CPU fallback).
83    pub fn analyze(&self, network: &AccountingNetwork) -> AnalysisResult {
84        let mut result = AnalysisResult::default();
85
86        // Suspense detection
87        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        // GAAP violation detection
96        result.gaap_violations = self.detect_gaap_violations(network);
97
98        // Fraud pattern detection
99        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        // Update stats
112        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    /// Detect GAAP violations.
122    fn detect_gaap_violations(&self, network: &AccountingNetwork) -> Vec<GaapViolation> {
123        let mut violations = Vec::new();
124
125        // Check for Revenue→Cash direct (should go through A/R)
126        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                // Revenue directly to Cash
132                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                // Revenue to Expense (improper offset)
145                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    /// Check for Benford's Law violations.
163    fn check_benford_violation(&self, network: &AccountingNetwork) -> Option<FraudPattern> {
164        // Count first digits from flow amounts
165        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; // Not enough data
181        }
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    /// Get first significant digit.
195    fn first_digit(mut n: u128) -> u32 {
196        while n >= 10 {
197            n /= 10;
198        }
199        n as u32
200    }
201
202    /// Detect circular flows (simplified DFS-based).
203    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        // Build adjacency list
212        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        // Simple cycle detection using DFS
222        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    /// DFS helper for cycle detection.
250    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}