datasynth_eval/coherence/
cross_process.rs1use crate::error::EvalResult;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct CrossProcessThresholds {
13 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#[derive(Debug, Clone)]
27pub struct CrossProcessLinkData {
28 pub inventory_total: usize,
30 pub inventory_linked: usize,
32 pub payment_total: usize,
34 pub payment_linked: usize,
36 pub ic_bilateral_total: usize,
38 pub ic_bilateral_traced: usize,
40 pub lineage_total: usize,
42 pub lineage_complete: usize,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct CrossProcessEvaluation {
49 pub inventory_p2p_o2c_link_rate: f64,
51 pub payment_bank_link_rate: f64,
53 pub ic_bilateral_trace_rate: f64,
55 pub overall_lineage_completeness: f64,
57 pub combined_score: f64,
59 pub passes: bool,
61 pub issues: Vec<String>,
63}
64
65pub struct CrossProcessEvaluator {
67 thresholds: CrossProcessThresholds,
68}
69
70impl CrossProcessEvaluator {
71 pub fn new() -> Self {
73 Self {
74 thresholds: CrossProcessThresholds::default(),
75 }
76 }
77
78 pub fn with_thresholds(thresholds: CrossProcessThresholds) -> Self {
80 Self { thresholds }
81 }
82
83 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 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}