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 {:.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}