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 {:.3} < {:.3}",
135                inventory_rate, min_rate
136            ));
137        }
138        if data.payment_total > 0 && payment_rate < min_rate {
139            issues.push(format!(
140                "Payment↔Bank link rate {:.3} < {:.3}",
141                payment_rate, min_rate
142            ));
143        }
144        if data.ic_bilateral_total > 0 && ic_rate < min_rate {
145            issues.push(format!(
146                "IC bilateral trace rate {:.3} < {:.3}",
147                ic_rate, min_rate
148            ));
149        }
150
151        let passes = issues.is_empty();
152
153        Ok(CrossProcessEvaluation {
154            inventory_p2p_o2c_link_rate: inventory_rate,
155            payment_bank_link_rate: payment_rate,
156            ic_bilateral_trace_rate: ic_rate,
157            overall_lineage_completeness: lineage_rate,
158            combined_score,
159            passes,
160            issues,
161        })
162    }
163}
164
165impl Default for CrossProcessEvaluator {
166    fn default() -> Self {
167        Self::new()
168    }
169}
170
171#[cfg(test)]
172#[allow(clippy::unwrap_used)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn test_fully_linked() {
178        let evaluator = CrossProcessEvaluator::new();
179        let data = CrossProcessLinkData {
180            inventory_total: 100,
181            inventory_linked: 95,
182            payment_total: 200,
183            payment_linked: 190,
184            ic_bilateral_total: 50,
185            ic_bilateral_traced: 48,
186            lineage_total: 300,
187            lineage_complete: 280,
188        };
189
190        let result = evaluator.evaluate(&data).unwrap();
191        assert!(result.passes);
192        assert!(result.combined_score > 0.9);
193    }
194
195    #[test]
196    fn test_low_link_rates() {
197        let evaluator = CrossProcessEvaluator::new();
198        let data = CrossProcessLinkData {
199            inventory_total: 100,
200            inventory_linked: 50,
201            payment_total: 100,
202            payment_linked: 40,
203            ic_bilateral_total: 0,
204            ic_bilateral_traced: 0,
205            lineage_total: 0,
206            lineage_complete: 0,
207        };
208
209        let result = evaluator.evaluate(&data).unwrap();
210        assert!(!result.passes);
211        assert_eq!(result.inventory_p2p_o2c_link_rate, 0.5);
212    }
213
214    #[test]
215    fn test_empty_data() {
216        let evaluator = CrossProcessEvaluator::new();
217        let data = CrossProcessLinkData {
218            inventory_total: 0,
219            inventory_linked: 0,
220            payment_total: 0,
221            payment_linked: 0,
222            ic_bilateral_total: 0,
223            ic_bilateral_traced: 0,
224            lineage_total: 0,
225            lineage_complete: 0,
226        };
227
228        let result = evaluator.evaluate(&data).unwrap();
229        assert!(result.passes);
230        assert_eq!(result.combined_score, 1.0);
231    }
232}