1use crate::{ComprehensiveEvaluation, EvaluationThresholds};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ConfigPatch {
13 pub path: String,
15 pub current_value: Option<String>,
17 pub suggested_value: String,
19 pub confidence: f64,
21 pub expected_impact: String,
23}
24
25impl ConfigPatch {
26 pub fn new(path: impl Into<String>, suggested_value: impl Into<String>) -> Self {
28 Self {
29 path: path.into(),
30 current_value: None,
31 suggested_value: suggested_value.into(),
32 confidence: 0.5,
33 expected_impact: String::new(),
34 }
35 }
36
37 pub fn with_current(mut self, value: impl Into<String>) -> Self {
39 self.current_value = Some(value.into());
40 self
41 }
42
43 pub fn with_confidence(mut self, confidence: f64) -> Self {
45 self.confidence = confidence.clamp(0.0, 1.0);
46 self
47 }
48
49 pub fn with_impact(mut self, impact: impl Into<String>) -> Self {
51 self.expected_impact = impact.into();
52 self
53 }
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct AutoTuneResult {
59 pub patches: Vec<ConfigPatch>,
61 pub expected_improvement: f64,
63 pub addressed_metrics: Vec<String>,
65 pub unaddressable_metrics: Vec<String>,
67 pub summary: String,
69}
70
71impl AutoTuneResult {
72 pub fn new() -> Self {
74 Self {
75 patches: Vec::new(),
76 expected_improvement: 0.0,
77 addressed_metrics: Vec::new(),
78 unaddressable_metrics: Vec::new(),
79 summary: String::new(),
80 }
81 }
82
83 pub fn has_patches(&self) -> bool {
85 !self.patches.is_empty()
86 }
87
88 pub fn patches_by_confidence(&self) -> Vec<&ConfigPatch> {
90 let mut sorted: Vec<_> = self.patches.iter().collect();
91 sorted.sort_by(|a, b| {
92 b.confidence
93 .partial_cmp(&a.confidence)
94 .unwrap_or(std::cmp::Ordering::Equal)
95 });
96 sorted
97 }
98}
99
100impl Default for AutoTuneResult {
101 fn default() -> Self {
102 Self::new()
103 }
104}
105
106#[derive(Debug, Clone)]
108pub struct MetricGap {
109 pub metric_name: String,
111 pub current_value: f64,
113 pub target_value: f64,
115 pub gap: f64,
117 pub is_minimum: bool,
119 pub config_paths: Vec<String>,
121}
122
123impl MetricGap {
124 pub fn severity(&self) -> f64 {
126 if self.target_value == 0.0 {
127 if self.gap.abs() > 0.0 {
128 1.0
129 } else {
130 0.0
131 }
132 } else {
133 (self.gap.abs() / self.target_value.abs()).min(1.0)
134 }
135 }
136}
137
138pub struct AutoTuner {
140 thresholds: EvaluationThresholds,
142 metric_mappings: HashMap<String, Vec<MetricConfigMapping>>,
144}
145
146#[derive(Debug, Clone)]
148struct MetricConfigMapping {
149 config_path: String,
151 influence: f64,
153 compute_value: ComputeStrategy,
155}
156
157#[allow(dead_code)] #[derive(Debug, Clone, Copy)]
160enum ComputeStrategy {
161 EnableBoolean,
163 SetFixed(f64),
165 IncreaseByGap,
167 DecreaseByGap,
169 SetToTarget,
171 MultiplyByGapFactor,
173}
174
175impl AutoTuner {
176 pub fn new() -> Self {
178 Self::with_thresholds(EvaluationThresholds::default())
179 }
180
181 pub fn with_thresholds(thresholds: EvaluationThresholds) -> Self {
183 let mut tuner = Self {
184 thresholds,
185 metric_mappings: HashMap::new(),
186 };
187 tuner.initialize_mappings();
188 tuner
189 }
190
191 fn initialize_mappings(&mut self) {
193 self.metric_mappings.insert(
195 "benford_p_value".to_string(),
196 vec![MetricConfigMapping {
197 config_path: "transactions.amount.benford_compliance".to_string(),
198 influence: 0.9,
199 compute_value: ComputeStrategy::EnableBoolean,
200 }],
201 );
202
203 self.metric_mappings.insert(
205 "round_number_ratio".to_string(),
206 vec![MetricConfigMapping {
207 config_path: "transactions.amount.round_number_bias".to_string(),
208 influence: 0.95,
209 compute_value: ComputeStrategy::SetToTarget,
210 }],
211 );
212
213 self.metric_mappings.insert(
215 "temporal_correlation".to_string(),
216 vec![MetricConfigMapping {
217 config_path: "transactions.temporal.seasonality_strength".to_string(),
218 influence: 0.7,
219 compute_value: ComputeStrategy::IncreaseByGap,
220 }],
221 );
222
223 self.metric_mappings.insert(
225 "anomaly_rate".to_string(),
226 vec![MetricConfigMapping {
227 config_path: "anomaly_injection.base_rate".to_string(),
228 influence: 0.95,
229 compute_value: ComputeStrategy::SetToTarget,
230 }],
231 );
232
233 self.metric_mappings.insert(
235 "label_coverage".to_string(),
236 vec![MetricConfigMapping {
237 config_path: "anomaly_injection.label_all".to_string(),
238 influence: 0.9,
239 compute_value: ComputeStrategy::EnableBoolean,
240 }],
241 );
242
243 self.metric_mappings.insert(
245 "duplicate_rate".to_string(),
246 vec![MetricConfigMapping {
247 config_path: "data_quality.duplicates.exact_rate".to_string(),
248 influence: 0.8,
249 compute_value: ComputeStrategy::SetToTarget,
250 }],
251 );
252
253 self.metric_mappings.insert(
255 "completeness_rate".to_string(),
256 vec![MetricConfigMapping {
257 config_path: "data_quality.missing_values.overall_rate".to_string(),
258 influence: 0.9,
259 compute_value: ComputeStrategy::DecreaseByGap,
260 }],
261 );
262
263 self.metric_mappings.insert(
265 "ic_match_rate".to_string(),
266 vec![MetricConfigMapping {
267 config_path: "intercompany.match_precision".to_string(),
268 influence: 0.85,
269 compute_value: ComputeStrategy::IncreaseByGap,
270 }],
271 );
272
273 self.metric_mappings.insert(
275 "doc_chain_completion".to_string(),
276 vec![
277 MetricConfigMapping {
278 config_path: "document_flows.p2p.completion_rate".to_string(),
279 influence: 0.5,
280 compute_value: ComputeStrategy::SetToTarget,
281 },
282 MetricConfigMapping {
283 config_path: "document_flows.o2c.completion_rate".to_string(),
284 influence: 0.5,
285 compute_value: ComputeStrategy::SetToTarget,
286 },
287 ],
288 );
289
290 self.metric_mappings.insert(
292 "graph_connectivity".to_string(),
293 vec![MetricConfigMapping {
294 config_path: "graph_export.ensure_connected".to_string(),
295 influence: 0.8,
296 compute_value: ComputeStrategy::EnableBoolean,
297 }],
298 );
299 }
300
301 pub fn analyze(&self, evaluation: &ComprehensiveEvaluation) -> AutoTuneResult {
303 let mut result = AutoTuneResult::new();
304
305 let gaps = self.identify_gaps(evaluation);
307
308 for gap in gaps {
310 if let Some(mappings) = self.metric_mappings.get(&gap.metric_name) {
311 for mapping in mappings {
312 if let Some(patch) = self.generate_patch(&gap, mapping) {
313 result.patches.push(patch);
314 if !result.addressed_metrics.contains(&gap.metric_name) {
315 result.addressed_metrics.push(gap.metric_name.clone());
316 }
317 }
318 }
319 } else {
320 if !result.unaddressable_metrics.contains(&gap.metric_name) {
321 result.unaddressable_metrics.push(gap.metric_name.clone());
322 }
323 }
324 }
325
326 if !result.patches.is_empty() {
328 let avg_confidence: f64 = result.patches.iter().map(|p| p.confidence).sum::<f64>()
329 / result.patches.len() as f64;
330 result.expected_improvement = avg_confidence;
331 }
332
333 result.summary = self.generate_summary(&result);
335
336 result
337 }
338
339 fn identify_gaps(&self, evaluation: &ComprehensiveEvaluation) -> Vec<MetricGap> {
341 let mut gaps = Vec::new();
342
343 if let Some(ref benford) = evaluation.statistical.benford {
345 if benford.p_value < self.thresholds.benford_p_value_min {
346 gaps.push(MetricGap {
347 metric_name: "benford_p_value".to_string(),
348 current_value: benford.p_value,
349 target_value: self.thresholds.benford_p_value_min,
350 gap: self.thresholds.benford_p_value_min - benford.p_value,
351 is_minimum: true,
352 config_paths: vec!["transactions.amount.benford_compliance".to_string()],
353 });
354 }
355 }
356
357 if let Some(ref amount) = evaluation.statistical.amount_distribution {
358 if amount.round_number_ratio < 0.05 {
359 gaps.push(MetricGap {
360 metric_name: "round_number_ratio".to_string(),
361 current_value: amount.round_number_ratio,
362 target_value: 0.10, gap: 0.10 - amount.round_number_ratio,
364 is_minimum: true,
365 config_paths: vec!["transactions.amount.round_number_bias".to_string()],
366 });
367 }
368 }
369
370 if let Some(ref temporal) = evaluation.statistical.temporal {
371 if temporal.pattern_correlation < self.thresholds.temporal_correlation_min {
372 gaps.push(MetricGap {
373 metric_name: "temporal_correlation".to_string(),
374 current_value: temporal.pattern_correlation,
375 target_value: self.thresholds.temporal_correlation_min,
376 gap: self.thresholds.temporal_correlation_min - temporal.pattern_correlation,
377 is_minimum: true,
378 config_paths: vec!["transactions.temporal.seasonality_strength".to_string()],
379 });
380 }
381 }
382
383 if let Some(ref ic) = evaluation.coherence.intercompany {
385 if ic.match_rate < self.thresholds.ic_match_rate_min {
386 gaps.push(MetricGap {
387 metric_name: "ic_match_rate".to_string(),
388 current_value: ic.match_rate,
389 target_value: self.thresholds.ic_match_rate_min,
390 gap: self.thresholds.ic_match_rate_min - ic.match_rate,
391 is_minimum: true,
392 config_paths: vec!["intercompany.match_precision".to_string()],
393 });
394 }
395 }
396
397 if let Some(ref doc_chain) = evaluation.coherence.document_chain {
398 let avg_completion =
399 (doc_chain.p2p_completion_rate + doc_chain.o2c_completion_rate) / 2.0;
400 if avg_completion < self.thresholds.document_chain_completion_min {
401 gaps.push(MetricGap {
402 metric_name: "doc_chain_completion".to_string(),
403 current_value: avg_completion,
404 target_value: self.thresholds.document_chain_completion_min,
405 gap: self.thresholds.document_chain_completion_min - avg_completion,
406 is_minimum: true,
407 config_paths: vec![
408 "document_flows.p2p.completion_rate".to_string(),
409 "document_flows.o2c.completion_rate".to_string(),
410 ],
411 });
412 }
413 }
414
415 if let Some(ref uniqueness) = evaluation.quality.uniqueness {
417 if uniqueness.duplicate_rate > self.thresholds.duplicate_rate_max {
418 gaps.push(MetricGap {
419 metric_name: "duplicate_rate".to_string(),
420 current_value: uniqueness.duplicate_rate,
421 target_value: self.thresholds.duplicate_rate_max,
422 gap: uniqueness.duplicate_rate - self.thresholds.duplicate_rate_max,
423 is_minimum: false, config_paths: vec!["data_quality.duplicates.exact_rate".to_string()],
425 });
426 }
427 }
428
429 if let Some(ref completeness) = evaluation.quality.completeness {
430 if completeness.overall_completeness < self.thresholds.completeness_rate_min {
431 gaps.push(MetricGap {
432 metric_name: "completeness_rate".to_string(),
433 current_value: completeness.overall_completeness,
434 target_value: self.thresholds.completeness_rate_min,
435 gap: self.thresholds.completeness_rate_min - completeness.overall_completeness,
436 is_minimum: true,
437 config_paths: vec!["data_quality.missing_values.overall_rate".to_string()],
438 });
439 }
440 }
441
442 if let Some(ref labels) = evaluation.ml_readiness.labels {
444 if labels.anomaly_rate < self.thresholds.anomaly_rate_min {
445 gaps.push(MetricGap {
446 metric_name: "anomaly_rate".to_string(),
447 current_value: labels.anomaly_rate,
448 target_value: self.thresholds.anomaly_rate_min,
449 gap: self.thresholds.anomaly_rate_min - labels.anomaly_rate,
450 is_minimum: true,
451 config_paths: vec!["anomaly_injection.base_rate".to_string()],
452 });
453 } else if labels.anomaly_rate > self.thresholds.anomaly_rate_max {
454 gaps.push(MetricGap {
455 metric_name: "anomaly_rate".to_string(),
456 current_value: labels.anomaly_rate,
457 target_value: self.thresholds.anomaly_rate_max,
458 gap: labels.anomaly_rate - self.thresholds.anomaly_rate_max,
459 is_minimum: false,
460 config_paths: vec!["anomaly_injection.base_rate".to_string()],
461 });
462 }
463
464 if labels.label_coverage < self.thresholds.label_coverage_min {
465 gaps.push(MetricGap {
466 metric_name: "label_coverage".to_string(),
467 current_value: labels.label_coverage,
468 target_value: self.thresholds.label_coverage_min,
469 gap: self.thresholds.label_coverage_min - labels.label_coverage,
470 is_minimum: true,
471 config_paths: vec!["anomaly_injection.label_all".to_string()],
472 });
473 }
474 }
475
476 if let Some(ref graph) = evaluation.ml_readiness.graph {
477 if graph.connectivity_score < self.thresholds.graph_connectivity_min {
478 gaps.push(MetricGap {
479 metric_name: "graph_connectivity".to_string(),
480 current_value: graph.connectivity_score,
481 target_value: self.thresholds.graph_connectivity_min,
482 gap: self.thresholds.graph_connectivity_min - graph.connectivity_score,
483 is_minimum: true,
484 config_paths: vec!["graph_export.ensure_connected".to_string()],
485 });
486 }
487 }
488
489 gaps
490 }
491
492 fn generate_patch(
494 &self,
495 gap: &MetricGap,
496 mapping: &MetricConfigMapping,
497 ) -> Option<ConfigPatch> {
498 let suggested_value = match mapping.compute_value {
499 ComputeStrategy::EnableBoolean => "true".to_string(),
500 ComputeStrategy::SetFixed(v) => format!("{:.4}", v),
501 ComputeStrategy::IncreaseByGap => format!("{:.4}", gap.current_value + gap.gap * 1.2),
502 ComputeStrategy::DecreaseByGap => {
503 format!("{:.4}", (gap.current_value - gap.gap * 1.2).max(0.0))
504 }
505 ComputeStrategy::SetToTarget => format!("{:.4}", gap.target_value),
506 ComputeStrategy::MultiplyByGapFactor => {
507 let factor = if gap.is_minimum {
508 1.0 + gap.severity() * 0.5
509 } else {
510 1.0 / (1.0 + gap.severity() * 0.5)
511 };
512 format!("{:.4}", gap.current_value * factor)
513 }
514 };
515
516 let confidence = mapping.influence * (1.0 - gap.severity() * 0.3);
517 let impact = format!(
518 "Should improve {} from {:.3} toward {:.3}",
519 gap.metric_name, gap.current_value, gap.target_value
520 );
521
522 Some(
523 ConfigPatch::new(&mapping.config_path, suggested_value)
524 .with_current(format!("{:.4}", gap.current_value))
525 .with_confidence(confidence)
526 .with_impact(impact),
527 )
528 }
529
530 fn generate_summary(&self, result: &AutoTuneResult) -> String {
532 if result.patches.is_empty() {
533 "No configuration changes suggested. All metrics meet thresholds.".to_string()
534 } else {
535 let high_confidence: Vec<_> = result
536 .patches
537 .iter()
538 .filter(|p| p.confidence > 0.7)
539 .collect();
540 let addressable = result.addressed_metrics.len();
541 let unaddressable = result.unaddressable_metrics.len();
542
543 format!(
544 "Suggested {} configuration changes ({} high-confidence). \
545 {} metrics can be improved, {} require manual investigation.",
546 result.patches.len(),
547 high_confidence.len(),
548 addressable,
549 unaddressable
550 )
551 }
552 }
553
554 pub fn thresholds(&self) -> &EvaluationThresholds {
556 &self.thresholds
557 }
558}
559
560impl Default for AutoTuner {
561 fn default() -> Self {
562 Self::new()
563 }
564}
565
566#[cfg(test)]
567mod tests {
568 use super::*;
569 use crate::statistical::{BenfordAnalysis, BenfordConformity};
570
571 #[test]
572 fn test_auto_tuner_creation() {
573 let tuner = AutoTuner::new();
574 assert!(!tuner.metric_mappings.is_empty());
575 }
576
577 #[test]
578 fn test_config_patch_builder() {
579 let patch = ConfigPatch::new("test.path", "value")
580 .with_current("old")
581 .with_confidence(0.8)
582 .with_impact("Should help");
583
584 assert_eq!(patch.path, "test.path");
585 assert_eq!(patch.current_value, Some("old".to_string()));
586 assert_eq!(patch.confidence, 0.8);
587 }
588
589 #[test]
590 fn test_auto_tune_result() {
591 let mut result = AutoTuneResult::new();
592 assert!(!result.has_patches());
593
594 result
595 .patches
596 .push(ConfigPatch::new("test", "value").with_confidence(0.9));
597 assert!(result.has_patches());
598
599 let sorted = result.patches_by_confidence();
600 assert_eq!(sorted.len(), 1);
601 }
602
603 #[test]
604 fn test_metric_gap_severity() {
605 let gap = MetricGap {
606 metric_name: "test".to_string(),
607 current_value: 0.02,
608 target_value: 0.05,
609 gap: 0.03,
610 is_minimum: true,
611 config_paths: vec![],
612 };
613
614 assert!((gap.severity() - 0.6).abs() < 0.001);
616 }
617
618 #[test]
619 fn test_analyze_empty_evaluation() {
620 let tuner = AutoTuner::new();
621 let evaluation = ComprehensiveEvaluation::new();
622
623 let result = tuner.analyze(&evaluation);
624
625 assert!(result.patches.is_empty());
627 }
628
629 #[test]
630 fn test_analyze_with_benford_gap() {
631 let tuner = AutoTuner::new();
632 let mut evaluation = ComprehensiveEvaluation::new();
633
634 evaluation.statistical.benford = Some(BenfordAnalysis {
636 sample_size: 1000,
637 observed_frequencies: [0.1; 9],
638 observed_counts: [100; 9],
639 expected_frequencies: [
640 0.301, 0.176, 0.125, 0.097, 0.079, 0.067, 0.058, 0.051, 0.046,
641 ],
642 chi_squared: 25.0,
643 degrees_of_freedom: 8,
644 p_value: 0.01, mad: 0.02,
646 conformity: BenfordConformity::NonConforming,
647 max_deviation: (1, 0.2), passes: false,
649 anti_benford_score: 0.5,
650 });
651
652 let result = tuner.analyze(&evaluation);
653
654 assert!(!result.patches.is_empty());
656 assert!(result
657 .addressed_metrics
658 .contains(&"benford_p_value".to_string()));
659 }
660}