Skip to main content

datasynth_eval/coherence/
cross_process.rs

1//! Cross-process link evaluator.
2//!
3//! Validates cross-process linkage including P2P-O2C via inventory,
4//! payment-bank reconciliation links, intercompany bilateral tracing,
5//! and overall lineage completeness.
6
7use crate::error::EvalResult;
8use serde::{Deserialize, Serialize};
9
10/// Thresholds for cross-process link evaluation.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct CrossProcessThresholds {
13    /// Minimum link rate for any cross-process category.
14    pub min_link_rate: f64,
15}
16
17impl Default for CrossProcessThresholds {
18    fn default() -> Self {
19        Self {
20            min_link_rate: 0.80,
21        }
22    }
23}
24
25/// Cross-process link data.
26#[derive(Debug, Clone)]
27pub struct CrossProcessLinkData {
28    /// Inventory P2P↔O2C links: total GoodsReceipt→Delivery candidates.
29    pub inventory_total: usize,
30    /// Inventory P2P↔O2C links: successfully linked.
31    pub inventory_linked: usize,
32    /// Payment↔BankReconciliation: total payment candidates.
33    pub payment_total: usize,
34    /// Payment↔BankReconciliation: successfully linked.
35    pub payment_linked: usize,
36    /// IC bilateral: total IC transaction pairs.
37    pub ic_bilateral_total: usize,
38    /// IC bilateral: pairs traced end-to-end.
39    pub ic_bilateral_traced: usize,
40    /// Total lineage entities.
41    pub lineage_total: usize,
42    /// Entities with complete lineage.
43    pub lineage_complete: usize,
44}
45
46/// Results of cross-process link evaluation.
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct CrossProcessEvaluation {
49    /// Inventory P2P↔O2C link rate.
50    pub inventory_p2p_o2c_link_rate: f64,
51    /// Payment↔BankReconciliation link rate.
52    pub payment_bank_link_rate: f64,
53    /// IC bilateral trace rate.
54    pub ic_bilateral_trace_rate: f64,
55    /// Overall lineage completeness.
56    pub overall_lineage_completeness: f64,
57    /// Combined cross-process score (average of all link rates).
58    pub combined_score: f64,
59    /// Overall pass/fail.
60    pub passes: bool,
61    /// Issues found.
62    pub issues: Vec<String>,
63}
64
65/// Evaluator for cross-process links.
66pub struct CrossProcessEvaluator {
67    thresholds: CrossProcessThresholds,
68}
69
70impl CrossProcessEvaluator {
71    /// Create a new evaluator with default thresholds.
72    pub fn new() -> Self {
73        Self {
74            thresholds: CrossProcessThresholds::default(),
75        }
76    }
77
78    /// Create with custom thresholds.
79    pub fn with_thresholds(thresholds: CrossProcessThresholds) -> Self {
80        Self { thresholds }
81    }
82
83    /// Evaluate cross-process link data.
84    pub fn evaluate(&self, data: &CrossProcessLinkData) -> EvalResult<CrossProcessEvaluation> {
85        let mut issues = Vec::new();
86
87        let inventory_rate = if data.inventory_total > 0 {
88            data.inventory_linked as f64 / data.inventory_total as f64
89        } else {
90            1.0
91        };
92
93        let payment_rate = if data.payment_total > 0 {
94            data.payment_linked as f64 / data.payment_total as f64
95        } else {
96            1.0
97        };
98
99        let ic_rate = if data.ic_bilateral_total > 0 {
100            data.ic_bilateral_traced as f64 / data.ic_bilateral_total as f64
101        } else {
102            1.0
103        };
104
105        let lineage_rate = if data.lineage_total > 0 {
106            data.lineage_complete as f64 / data.lineage_total as f64
107        } else {
108            1.0
109        };
110
111        // Compute combined score from available rates
112        let mut rates = Vec::new();
113        if data.inventory_total > 0 {
114            rates.push(inventory_rate);
115        }
116        if data.payment_total > 0 {
117            rates.push(payment_rate);
118        }
119        if data.ic_bilateral_total > 0 {
120            rates.push(ic_rate);
121        }
122        if data.lineage_total > 0 {
123            rates.push(lineage_rate);
124        }
125        let combined_score = if rates.is_empty() {
126            1.0
127        } else {
128            rates.iter().sum::<f64>() / rates.len() as f64
129        };
130
131        let min_rate = self.thresholds.min_link_rate;
132        if data.inventory_total > 0 && inventory_rate < min_rate {
133            issues.push(format!(
134                "Inventory P2P↔O2C link rate {inventory_rate:.3} < {min_rate:.3}"
135            ));
136        }
137        if data.payment_total > 0 && payment_rate < min_rate {
138            issues.push(format!(
139                "Payment↔Bank link rate {payment_rate:.3} < {min_rate:.3}"
140            ));
141        }
142        if data.ic_bilateral_total > 0 && ic_rate < min_rate {
143            issues.push(format!(
144                "IC bilateral trace rate {ic_rate:.3} < {min_rate:.3}"
145            ));
146        }
147
148        let passes = issues.is_empty();
149
150        Ok(CrossProcessEvaluation {
151            inventory_p2p_o2c_link_rate: inventory_rate,
152            payment_bank_link_rate: payment_rate,
153            ic_bilateral_trace_rate: ic_rate,
154            overall_lineage_completeness: lineage_rate,
155            combined_score,
156            passes,
157            issues,
158        })
159    }
160}
161
162impl Default for CrossProcessEvaluator {
163    fn default() -> Self {
164        Self::new()
165    }
166}
167
168#[cfg(test)]
169#[allow(clippy::unwrap_used)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn test_fully_linked() {
175        let evaluator = CrossProcessEvaluator::new();
176        let data = CrossProcessLinkData {
177            inventory_total: 100,
178            inventory_linked: 95,
179            payment_total: 200,
180            payment_linked: 190,
181            ic_bilateral_total: 50,
182            ic_bilateral_traced: 48,
183            lineage_total: 300,
184            lineage_complete: 280,
185        };
186
187        let result = evaluator.evaluate(&data).unwrap();
188        assert!(result.passes);
189        assert!(result.combined_score > 0.9);
190    }
191
192    #[test]
193    fn test_low_link_rates() {
194        let evaluator = CrossProcessEvaluator::new();
195        let data = CrossProcessLinkData {
196            inventory_total: 100,
197            inventory_linked: 50,
198            payment_total: 100,
199            payment_linked: 40,
200            ic_bilateral_total: 0,
201            ic_bilateral_traced: 0,
202            lineage_total: 0,
203            lineage_complete: 0,
204        };
205
206        let result = evaluator.evaluate(&data).unwrap();
207        assert!(!result.passes);
208        assert_eq!(result.inventory_p2p_o2c_link_rate, 0.5);
209    }
210
211    #[test]
212    fn test_empty_data() {
213        let evaluator = CrossProcessEvaluator::new();
214        let data = CrossProcessLinkData {
215            inventory_total: 0,
216            inventory_linked: 0,
217            payment_total: 0,
218            payment_linked: 0,
219            ic_bilateral_total: 0,
220            ic_bilateral_traced: 0,
221            lineage_total: 0,
222            lineage_complete: 0,
223        };
224
225        let result = evaluator.evaluate(&data).unwrap();
226        assert!(result.passes);
227        assert_eq!(result.combined_score, 1.0);
228    }
229}