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#[derive(Debug, Clone, Copy)]
159enum ComputeStrategy {
160 EnableBoolean,
162 SetFixed(f64),
164 IncreaseByGap,
166 DecreaseByGap,
168 SetToTarget,
170}
171
172impl AutoTuner {
173 pub fn new() -> Self {
175 Self::with_thresholds(EvaluationThresholds::default())
176 }
177
178 pub fn with_thresholds(thresholds: EvaluationThresholds) -> Self {
180 let mut tuner = Self {
181 thresholds,
182 metric_mappings: HashMap::new(),
183 };
184 tuner.initialize_mappings();
185 tuner
186 }
187
188 fn initialize_mappings(&mut self) {
190 self.metric_mappings.insert(
192 "benford_p_value".to_string(),
193 vec![MetricConfigMapping {
194 config_path: "transactions.amount.benford_compliance".to_string(),
195 influence: 0.9,
196 compute_value: ComputeStrategy::EnableBoolean,
197 }],
198 );
199
200 self.metric_mappings.insert(
202 "round_number_ratio".to_string(),
203 vec![MetricConfigMapping {
204 config_path: "transactions.amount.round_number_bias".to_string(),
205 influence: 0.95,
206 compute_value: ComputeStrategy::SetToTarget,
207 }],
208 );
209
210 self.metric_mappings.insert(
212 "temporal_correlation".to_string(),
213 vec![MetricConfigMapping {
214 config_path: "transactions.temporal.seasonality_strength".to_string(),
215 influence: 0.7,
216 compute_value: ComputeStrategy::IncreaseByGap,
217 }],
218 );
219
220 self.metric_mappings.insert(
222 "anomaly_rate".to_string(),
223 vec![MetricConfigMapping {
224 config_path: "anomaly_injection.base_rate".to_string(),
225 influence: 0.95,
226 compute_value: ComputeStrategy::SetToTarget,
227 }],
228 );
229
230 self.metric_mappings.insert(
232 "label_coverage".to_string(),
233 vec![MetricConfigMapping {
234 config_path: "anomaly_injection.label_all".to_string(),
235 influence: 0.9,
236 compute_value: ComputeStrategy::EnableBoolean,
237 }],
238 );
239
240 self.metric_mappings.insert(
242 "duplicate_rate".to_string(),
243 vec![MetricConfigMapping {
244 config_path: "data_quality.duplicates.exact_rate".to_string(),
245 influence: 0.8,
246 compute_value: ComputeStrategy::SetToTarget,
247 }],
248 );
249
250 self.metric_mappings.insert(
252 "completeness_rate".to_string(),
253 vec![MetricConfigMapping {
254 config_path: "data_quality.missing_values.overall_rate".to_string(),
255 influence: 0.9,
256 compute_value: ComputeStrategy::DecreaseByGap,
257 }],
258 );
259
260 self.metric_mappings.insert(
262 "ic_match_rate".to_string(),
263 vec![MetricConfigMapping {
264 config_path: "intercompany.match_precision".to_string(),
265 influence: 0.85,
266 compute_value: ComputeStrategy::IncreaseByGap,
267 }],
268 );
269
270 self.metric_mappings.insert(
272 "doc_chain_completion".to_string(),
273 vec![
274 MetricConfigMapping {
275 config_path: "document_flows.p2p.completion_rate".to_string(),
276 influence: 0.5,
277 compute_value: ComputeStrategy::SetToTarget,
278 },
279 MetricConfigMapping {
280 config_path: "document_flows.o2c.completion_rate".to_string(),
281 influence: 0.5,
282 compute_value: ComputeStrategy::SetToTarget,
283 },
284 ],
285 );
286
287 self.metric_mappings.insert(
289 "graph_connectivity".to_string(),
290 vec![MetricConfigMapping {
291 config_path: "graph_export.ensure_connected".to_string(),
292 influence: 0.8,
293 compute_value: ComputeStrategy::EnableBoolean,
294 }],
295 );
296
297 self.metric_mappings.insert(
301 "payroll_accuracy".to_string(),
302 vec![MetricConfigMapping {
303 config_path: "hr.payroll.calculation_precision".to_string(),
304 influence: 0.9,
305 compute_value: ComputeStrategy::SetToTarget,
306 }],
307 );
308
309 self.metric_mappings.insert(
311 "manufacturing_yield".to_string(),
312 vec![MetricConfigMapping {
313 config_path: "manufacturing.production_orders.yield_target".to_string(),
314 influence: 0.8,
315 compute_value: ComputeStrategy::SetToTarget,
316 }],
317 );
318
319 self.metric_mappings.insert(
321 "s2c_chain_completion".to_string(),
322 vec![MetricConfigMapping {
323 config_path: "source_to_pay.rfx_completion_rate".to_string(),
324 influence: 0.85,
325 compute_value: ComputeStrategy::SetToTarget,
326 }],
327 );
328
329 self.metric_mappings.insert(
331 "bank_recon_balance".to_string(),
332 vec![MetricConfigMapping {
333 config_path: "enterprise.bank_reconciliation.tolerance".to_string(),
334 influence: 0.9,
335 compute_value: ComputeStrategy::DecreaseByGap,
336 }],
337 );
338
339 self.metric_mappings.insert(
341 "financial_reporting_tie_back".to_string(),
342 vec![MetricConfigMapping {
343 config_path: "financial_reporting.statement_generation.enabled".to_string(),
344 influence: 0.85,
345 compute_value: ComputeStrategy::EnableBoolean,
346 }],
347 );
348
349 self.metric_mappings.insert(
351 "aml_detectability".to_string(),
352 vec![MetricConfigMapping {
353 config_path: "enterprise.banking.aml_typology_count".to_string(),
354 influence: 0.8,
355 compute_value: ComputeStrategy::IncreaseByGap,
356 }],
357 );
358
359 self.metric_mappings.insert(
361 "process_mining_coverage".to_string(),
362 vec![MetricConfigMapping {
363 config_path: "business_processes.ocel_enabled".to_string(),
364 influence: 0.85,
365 compute_value: ComputeStrategy::EnableBoolean,
366 }],
367 );
368
369 self.metric_mappings.insert(
371 "audit_evidence_coverage".to_string(),
372 vec![MetricConfigMapping {
373 config_path: "audit_standards.evidence_per_finding".to_string(),
374 influence: 0.8,
375 compute_value: ComputeStrategy::IncreaseByGap,
376 }],
377 );
378
379 self.metric_mappings.insert(
381 "anomaly_separability".to_string(),
382 vec![MetricConfigMapping {
383 config_path: "anomaly_injection.base_rate".to_string(),
384 influence: 0.75,
385 compute_value: ComputeStrategy::IncreaseByGap,
386 }],
387 );
388
389 self.metric_mappings.insert(
391 "feature_quality".to_string(),
392 vec![MetricConfigMapping {
393 config_path: "graph_export.feature_completeness".to_string(),
394 influence: 0.7,
395 compute_value: ComputeStrategy::EnableBoolean,
396 }],
397 );
398
399 self.metric_mappings.insert(
401 "gnn_readiness".to_string(),
402 vec![
403 MetricConfigMapping {
404 config_path: "graph_export.ensure_connected".to_string(),
405 influence: 0.6,
406 compute_value: ComputeStrategy::EnableBoolean,
407 },
408 MetricConfigMapping {
409 config_path: "cross_process_links.enabled".to_string(),
410 influence: 0.5,
411 compute_value: ComputeStrategy::EnableBoolean,
412 },
413 ],
414 );
415
416 self.metric_mappings.insert(
418 "domain_gap".to_string(),
419 vec![MetricConfigMapping {
420 config_path: "distributions.industry_profile".to_string(),
421 influence: 0.7,
422 compute_value: ComputeStrategy::SetFixed(1.0), }],
424 );
425 }
426
427 pub fn analyze(&self, evaluation: &ComprehensiveEvaluation) -> AutoTuneResult {
429 let mut result = AutoTuneResult::new();
430
431 let gaps = self.identify_gaps(evaluation);
433
434 for gap in gaps {
436 if let Some(mappings) = self.metric_mappings.get(&gap.metric_name) {
437 for mapping in mappings {
438 if let Some(patch) = self.generate_patch(&gap, mapping) {
439 result.patches.push(patch);
440 if !result.addressed_metrics.contains(&gap.metric_name) {
441 result.addressed_metrics.push(gap.metric_name.clone());
442 }
443 }
444 }
445 } else if !result.unaddressable_metrics.contains(&gap.metric_name) {
446 result.unaddressable_metrics.push(gap.metric_name.clone());
447 }
448 }
449
450 if !result.patches.is_empty() {
452 let avg_confidence: f64 = result.patches.iter().map(|p| p.confidence).sum::<f64>()
453 / result.patches.len() as f64;
454 result.expected_improvement = avg_confidence;
455 }
456
457 result.summary = self.generate_summary(&result);
459
460 result
461 }
462
463 fn identify_gaps(&self, evaluation: &ComprehensiveEvaluation) -> Vec<MetricGap> {
465 let mut gaps = Vec::new();
466
467 if let Some(ref benford) = evaluation.statistical.benford {
469 if benford.p_value < self.thresholds.benford_p_value_min {
470 gaps.push(MetricGap {
471 metric_name: "benford_p_value".to_string(),
472 current_value: benford.p_value,
473 target_value: self.thresholds.benford_p_value_min,
474 gap: self.thresholds.benford_p_value_min - benford.p_value,
475 is_minimum: true,
476 config_paths: vec!["transactions.amount.benford_compliance".to_string()],
477 });
478 }
479 }
480
481 if let Some(ref amount) = evaluation.statistical.amount_distribution {
482 if amount.round_number_ratio < 0.05 {
483 gaps.push(MetricGap {
484 metric_name: "round_number_ratio".to_string(),
485 current_value: amount.round_number_ratio,
486 target_value: 0.10, gap: 0.10 - amount.round_number_ratio,
488 is_minimum: true,
489 config_paths: vec!["transactions.amount.round_number_bias".to_string()],
490 });
491 }
492 }
493
494 if let Some(ref temporal) = evaluation.statistical.temporal {
495 if temporal.pattern_correlation < self.thresholds.temporal_correlation_min {
496 gaps.push(MetricGap {
497 metric_name: "temporal_correlation".to_string(),
498 current_value: temporal.pattern_correlation,
499 target_value: self.thresholds.temporal_correlation_min,
500 gap: self.thresholds.temporal_correlation_min - temporal.pattern_correlation,
501 is_minimum: true,
502 config_paths: vec!["transactions.temporal.seasonality_strength".to_string()],
503 });
504 }
505 }
506
507 if let Some(ref ic) = evaluation.coherence.intercompany {
509 if ic.match_rate < self.thresholds.ic_match_rate_min {
510 gaps.push(MetricGap {
511 metric_name: "ic_match_rate".to_string(),
512 current_value: ic.match_rate,
513 target_value: self.thresholds.ic_match_rate_min,
514 gap: self.thresholds.ic_match_rate_min - ic.match_rate,
515 is_minimum: true,
516 config_paths: vec!["intercompany.match_precision".to_string()],
517 });
518 }
519 }
520
521 if let Some(ref doc_chain) = evaluation.coherence.document_chain {
522 let avg_completion =
523 (doc_chain.p2p_completion_rate + doc_chain.o2c_completion_rate) / 2.0;
524 if avg_completion < self.thresholds.document_chain_completion_min {
525 gaps.push(MetricGap {
526 metric_name: "doc_chain_completion".to_string(),
527 current_value: avg_completion,
528 target_value: self.thresholds.document_chain_completion_min,
529 gap: self.thresholds.document_chain_completion_min - avg_completion,
530 is_minimum: true,
531 config_paths: vec![
532 "document_flows.p2p.completion_rate".to_string(),
533 "document_flows.o2c.completion_rate".to_string(),
534 ],
535 });
536 }
537 }
538
539 if let Some(ref uniqueness) = evaluation.quality.uniqueness {
541 if uniqueness.duplicate_rate > self.thresholds.duplicate_rate_max {
542 gaps.push(MetricGap {
543 metric_name: "duplicate_rate".to_string(),
544 current_value: uniqueness.duplicate_rate,
545 target_value: self.thresholds.duplicate_rate_max,
546 gap: uniqueness.duplicate_rate - self.thresholds.duplicate_rate_max,
547 is_minimum: false, config_paths: vec!["data_quality.duplicates.exact_rate".to_string()],
549 });
550 }
551 }
552
553 if let Some(ref completeness) = evaluation.quality.completeness {
554 if completeness.overall_completeness < self.thresholds.completeness_rate_min {
555 gaps.push(MetricGap {
556 metric_name: "completeness_rate".to_string(),
557 current_value: completeness.overall_completeness,
558 target_value: self.thresholds.completeness_rate_min,
559 gap: self.thresholds.completeness_rate_min - completeness.overall_completeness,
560 is_minimum: true,
561 config_paths: vec!["data_quality.missing_values.overall_rate".to_string()],
562 });
563 }
564 }
565
566 if let Some(ref labels) = evaluation.ml_readiness.labels {
568 if labels.anomaly_rate < self.thresholds.anomaly_rate_min {
569 gaps.push(MetricGap {
570 metric_name: "anomaly_rate".to_string(),
571 current_value: labels.anomaly_rate,
572 target_value: self.thresholds.anomaly_rate_min,
573 gap: self.thresholds.anomaly_rate_min - labels.anomaly_rate,
574 is_minimum: true,
575 config_paths: vec!["anomaly_injection.base_rate".to_string()],
576 });
577 } else if labels.anomaly_rate > self.thresholds.anomaly_rate_max {
578 gaps.push(MetricGap {
579 metric_name: "anomaly_rate".to_string(),
580 current_value: labels.anomaly_rate,
581 target_value: self.thresholds.anomaly_rate_max,
582 gap: labels.anomaly_rate - self.thresholds.anomaly_rate_max,
583 is_minimum: false,
584 config_paths: vec!["anomaly_injection.base_rate".to_string()],
585 });
586 }
587
588 if labels.label_coverage < self.thresholds.label_coverage_min {
589 gaps.push(MetricGap {
590 metric_name: "label_coverage".to_string(),
591 current_value: labels.label_coverage,
592 target_value: self.thresholds.label_coverage_min,
593 gap: self.thresholds.label_coverage_min - labels.label_coverage,
594 is_minimum: true,
595 config_paths: vec!["anomaly_injection.label_all".to_string()],
596 });
597 }
598 }
599
600 if let Some(ref graph) = evaluation.ml_readiness.graph {
601 if graph.connectivity_score < self.thresholds.graph_connectivity_min {
602 gaps.push(MetricGap {
603 metric_name: "graph_connectivity".to_string(),
604 current_value: graph.connectivity_score,
605 target_value: self.thresholds.graph_connectivity_min,
606 gap: self.thresholds.graph_connectivity_min - graph.connectivity_score,
607 is_minimum: true,
608 config_paths: vec!["graph_export.ensure_connected".to_string()],
609 });
610 }
611 }
612
613 if let Some(ref hr) = evaluation.coherence.hr_payroll {
617 if hr.gross_to_net_accuracy < 0.999 {
618 gaps.push(MetricGap {
619 metric_name: "payroll_accuracy".to_string(),
620 current_value: hr.gross_to_net_accuracy,
621 target_value: 0.999,
622 gap: 0.999 - hr.gross_to_net_accuracy,
623 is_minimum: true,
624 config_paths: vec!["hr.payroll.calculation_precision".to_string()],
625 });
626 }
627 }
628
629 if let Some(ref mfg) = evaluation.coherence.manufacturing {
631 if mfg.yield_rate_consistency < 0.95 {
632 gaps.push(MetricGap {
633 metric_name: "manufacturing_yield".to_string(),
634 current_value: mfg.yield_rate_consistency,
635 target_value: 0.95,
636 gap: 0.95 - mfg.yield_rate_consistency,
637 is_minimum: true,
638 config_paths: vec!["manufacturing.production_orders.yield_target".to_string()],
639 });
640 }
641 }
642
643 if let Some(ref sourcing) = evaluation.coherence.sourcing {
645 if sourcing.rfx_completion_rate < 0.90 {
646 gaps.push(MetricGap {
647 metric_name: "s2c_chain_completion".to_string(),
648 current_value: sourcing.rfx_completion_rate,
649 target_value: 0.90,
650 gap: 0.90 - sourcing.rfx_completion_rate,
651 is_minimum: true,
652 config_paths: vec!["source_to_pay.rfx_completion_rate".to_string()],
653 });
654 }
655 }
656
657 if let Some(ref as_eval) = evaluation.ml_readiness.anomaly_scoring {
659 if as_eval.anomaly_separability < self.thresholds.min_anomaly_separability {
660 gaps.push(MetricGap {
661 metric_name: "anomaly_separability".to_string(),
662 current_value: as_eval.anomaly_separability,
663 target_value: self.thresholds.min_anomaly_separability,
664 gap: self.thresholds.min_anomaly_separability - as_eval.anomaly_separability,
665 is_minimum: true,
666 config_paths: vec!["anomaly_injection.base_rate".to_string()],
667 });
668 }
669 }
670
671 if let Some(ref fq_eval) = evaluation.ml_readiness.feature_quality {
673 if fq_eval.feature_quality_score < self.thresholds.min_feature_quality {
674 gaps.push(MetricGap {
675 metric_name: "feature_quality".to_string(),
676 current_value: fq_eval.feature_quality_score,
677 target_value: self.thresholds.min_feature_quality,
678 gap: self.thresholds.min_feature_quality - fq_eval.feature_quality_score,
679 is_minimum: true,
680 config_paths: vec!["graph_export.feature_completeness".to_string()],
681 });
682 }
683 }
684
685 if let Some(ref gnn_eval) = evaluation.ml_readiness.gnn_readiness {
687 if gnn_eval.gnn_readiness_score < self.thresholds.min_gnn_readiness {
688 gaps.push(MetricGap {
689 metric_name: "gnn_readiness".to_string(),
690 current_value: gnn_eval.gnn_readiness_score,
691 target_value: self.thresholds.min_gnn_readiness,
692 gap: self.thresholds.min_gnn_readiness - gnn_eval.gnn_readiness_score,
693 is_minimum: true,
694 config_paths: vec![
695 "graph_export.ensure_connected".to_string(),
696 "cross_process_links.enabled".to_string(),
697 ],
698 });
699 }
700 }
701
702 if let Some(ref dg_eval) = evaluation.ml_readiness.domain_gap {
704 if dg_eval.domain_gap_score > self.thresholds.max_domain_gap {
705 gaps.push(MetricGap {
706 metric_name: "domain_gap".to_string(),
707 current_value: dg_eval.domain_gap_score,
708 target_value: self.thresholds.max_domain_gap,
709 gap: dg_eval.domain_gap_score - self.thresholds.max_domain_gap,
710 is_minimum: false,
711 config_paths: vec!["distributions.industry_profile".to_string()],
712 });
713 }
714 }
715
716 gaps
717 }
718
719 fn generate_patch(
721 &self,
722 gap: &MetricGap,
723 mapping: &MetricConfigMapping,
724 ) -> Option<ConfigPatch> {
725 let suggested_value = match mapping.compute_value {
726 ComputeStrategy::EnableBoolean => "true".to_string(),
727 ComputeStrategy::SetFixed(v) => format!("{:.4}", v),
728 ComputeStrategy::IncreaseByGap => format!("{:.4}", gap.current_value + gap.gap * 1.2),
729 ComputeStrategy::DecreaseByGap => {
730 format!("{:.4}", (gap.current_value - gap.gap * 1.2).max(0.0))
731 }
732 ComputeStrategy::SetToTarget => format!("{:.4}", gap.target_value),
733 };
734
735 let confidence = mapping.influence * (1.0 - gap.severity() * 0.3);
736 let impact = format!(
737 "Should improve {} from {:.3} toward {:.3}",
738 gap.metric_name, gap.current_value, gap.target_value
739 );
740
741 Some(
742 ConfigPatch::new(&mapping.config_path, suggested_value)
743 .with_current(format!("{:.4}", gap.current_value))
744 .with_confidence(confidence)
745 .with_impact(impact),
746 )
747 }
748
749 fn generate_summary(&self, result: &AutoTuneResult) -> String {
751 if result.patches.is_empty() {
752 "No configuration changes suggested. All metrics meet thresholds.".to_string()
753 } else {
754 let high_confidence: Vec<_> = result
755 .patches
756 .iter()
757 .filter(|p| p.confidence > 0.7)
758 .collect();
759 let addressable = result.addressed_metrics.len();
760 let unaddressable = result.unaddressable_metrics.len();
761
762 format!(
763 "Suggested {} configuration changes ({} high-confidence). \
764 {} metrics can be improved, {} require manual investigation.",
765 result.patches.len(),
766 high_confidence.len(),
767 addressable,
768 unaddressable
769 )
770 }
771 }
772
773 pub fn thresholds(&self) -> &EvaluationThresholds {
775 &self.thresholds
776 }
777}
778
779impl Default for AutoTuner {
780 fn default() -> Self {
781 Self::new()
782 }
783}
784
785#[cfg(test)]
786#[allow(clippy::unwrap_used)]
787mod tests {
788 use super::*;
789 use crate::statistical::{BenfordAnalysis, BenfordConformity};
790
791 #[test]
792 fn test_auto_tuner_creation() {
793 let tuner = AutoTuner::new();
794 assert!(!tuner.metric_mappings.is_empty());
795 }
796
797 #[test]
798 fn test_config_patch_builder() {
799 let patch = ConfigPatch::new("test.path", "value")
800 .with_current("old")
801 .with_confidence(0.8)
802 .with_impact("Should help");
803
804 assert_eq!(patch.path, "test.path");
805 assert_eq!(patch.current_value, Some("old".to_string()));
806 assert_eq!(patch.confidence, 0.8);
807 }
808
809 #[test]
810 fn test_auto_tune_result() {
811 let mut result = AutoTuneResult::new();
812 assert!(!result.has_patches());
813
814 result
815 .patches
816 .push(ConfigPatch::new("test", "value").with_confidence(0.9));
817 assert!(result.has_patches());
818
819 let sorted = result.patches_by_confidence();
820 assert_eq!(sorted.len(), 1);
821 }
822
823 #[test]
824 fn test_metric_gap_severity() {
825 let gap = MetricGap {
826 metric_name: "test".to_string(),
827 current_value: 0.02,
828 target_value: 0.05,
829 gap: 0.03,
830 is_minimum: true,
831 config_paths: vec![],
832 };
833
834 assert!((gap.severity() - 0.6).abs() < 0.001);
836 }
837
838 #[test]
839 fn test_analyze_empty_evaluation() {
840 let tuner = AutoTuner::new();
841 let evaluation = ComprehensiveEvaluation::new();
842
843 let result = tuner.analyze(&evaluation);
844
845 assert!(result.patches.is_empty());
847 }
848
849 #[test]
850 fn test_analyze_with_benford_gap() {
851 let tuner = AutoTuner::new();
852 let mut evaluation = ComprehensiveEvaluation::new();
853
854 evaluation.statistical.benford = Some(BenfordAnalysis {
856 sample_size: 1000,
857 observed_frequencies: [0.1; 9],
858 observed_counts: [100; 9],
859 expected_frequencies: [
860 0.301, 0.176, 0.125, 0.097, 0.079, 0.067, 0.058, 0.051, 0.046,
861 ],
862 chi_squared: 25.0,
863 degrees_of_freedom: 8,
864 p_value: 0.01, mad: 0.02,
866 conformity: BenfordConformity::NonConforming,
867 max_deviation: (1, 0.2), passes: false,
869 anti_benford_score: 0.5,
870 });
871
872 let result = tuner.analyze(&evaluation);
873
874 assert!(!result.patches.is_empty());
876 assert!(result
877 .addressed_metrics
878 .contains(&"benford_p_value".to_string()));
879 }
880}