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 self.metric_mappings.insert(
304 "payroll_accuracy".to_string(),
305 vec![MetricConfigMapping {
306 config_path: "hr.payroll.calculation_precision".to_string(),
307 influence: 0.9,
308 compute_value: ComputeStrategy::SetToTarget,
309 }],
310 );
311
312 self.metric_mappings.insert(
314 "manufacturing_yield".to_string(),
315 vec![MetricConfigMapping {
316 config_path: "manufacturing.production_orders.yield_target".to_string(),
317 influence: 0.8,
318 compute_value: ComputeStrategy::SetToTarget,
319 }],
320 );
321
322 self.metric_mappings.insert(
324 "s2c_chain_completion".to_string(),
325 vec![MetricConfigMapping {
326 config_path: "source_to_pay.rfx_completion_rate".to_string(),
327 influence: 0.85,
328 compute_value: ComputeStrategy::SetToTarget,
329 }],
330 );
331
332 self.metric_mappings.insert(
334 "bank_recon_balance".to_string(),
335 vec![MetricConfigMapping {
336 config_path: "enterprise.bank_reconciliation.tolerance".to_string(),
337 influence: 0.9,
338 compute_value: ComputeStrategy::DecreaseByGap,
339 }],
340 );
341
342 self.metric_mappings.insert(
344 "financial_reporting_tie_back".to_string(),
345 vec![MetricConfigMapping {
346 config_path: "financial_reporting.statement_generation.enabled".to_string(),
347 influence: 0.85,
348 compute_value: ComputeStrategy::EnableBoolean,
349 }],
350 );
351
352 self.metric_mappings.insert(
354 "aml_detectability".to_string(),
355 vec![MetricConfigMapping {
356 config_path: "enterprise.banking.aml_typology_count".to_string(),
357 influence: 0.8,
358 compute_value: ComputeStrategy::IncreaseByGap,
359 }],
360 );
361
362 self.metric_mappings.insert(
364 "process_mining_coverage".to_string(),
365 vec![MetricConfigMapping {
366 config_path: "business_processes.ocel_enabled".to_string(),
367 influence: 0.85,
368 compute_value: ComputeStrategy::EnableBoolean,
369 }],
370 );
371
372 self.metric_mappings.insert(
374 "audit_evidence_coverage".to_string(),
375 vec![MetricConfigMapping {
376 config_path: "audit_standards.evidence_per_finding".to_string(),
377 influence: 0.8,
378 compute_value: ComputeStrategy::IncreaseByGap,
379 }],
380 );
381
382 self.metric_mappings.insert(
384 "anomaly_separability".to_string(),
385 vec![MetricConfigMapping {
386 config_path: "anomaly_injection.base_rate".to_string(),
387 influence: 0.75,
388 compute_value: ComputeStrategy::IncreaseByGap,
389 }],
390 );
391
392 self.metric_mappings.insert(
394 "feature_quality".to_string(),
395 vec![MetricConfigMapping {
396 config_path: "graph_export.feature_completeness".to_string(),
397 influence: 0.7,
398 compute_value: ComputeStrategy::EnableBoolean,
399 }],
400 );
401
402 self.metric_mappings.insert(
404 "gnn_readiness".to_string(),
405 vec![
406 MetricConfigMapping {
407 config_path: "graph_export.ensure_connected".to_string(),
408 influence: 0.6,
409 compute_value: ComputeStrategy::EnableBoolean,
410 },
411 MetricConfigMapping {
412 config_path: "cross_process_links.enabled".to_string(),
413 influence: 0.5,
414 compute_value: ComputeStrategy::EnableBoolean,
415 },
416 ],
417 );
418
419 self.metric_mappings.insert(
421 "domain_gap".to_string(),
422 vec![MetricConfigMapping {
423 config_path: "distributions.industry_profile".to_string(),
424 influence: 0.7,
425 compute_value: ComputeStrategy::SetFixed(1.0), }],
427 );
428 }
429
430 pub fn analyze(&self, evaluation: &ComprehensiveEvaluation) -> AutoTuneResult {
432 let mut result = AutoTuneResult::new();
433
434 let gaps = self.identify_gaps(evaluation);
436
437 for gap in gaps {
439 if let Some(mappings) = self.metric_mappings.get(&gap.metric_name) {
440 for mapping in mappings {
441 if let Some(patch) = self.generate_patch(&gap, mapping) {
442 result.patches.push(patch);
443 if !result.addressed_metrics.contains(&gap.metric_name) {
444 result.addressed_metrics.push(gap.metric_name.clone());
445 }
446 }
447 }
448 } else if !result.unaddressable_metrics.contains(&gap.metric_name) {
449 result.unaddressable_metrics.push(gap.metric_name.clone());
450 }
451 }
452
453 if !result.patches.is_empty() {
455 let avg_confidence: f64 = result.patches.iter().map(|p| p.confidence).sum::<f64>()
456 / result.patches.len() as f64;
457 result.expected_improvement = avg_confidence;
458 }
459
460 result.summary = self.generate_summary(&result);
462
463 result
464 }
465
466 fn identify_gaps(&self, evaluation: &ComprehensiveEvaluation) -> Vec<MetricGap> {
468 let mut gaps = Vec::new();
469
470 if let Some(ref benford) = evaluation.statistical.benford {
472 if benford.p_value < self.thresholds.benford_p_value_min {
473 gaps.push(MetricGap {
474 metric_name: "benford_p_value".to_string(),
475 current_value: benford.p_value,
476 target_value: self.thresholds.benford_p_value_min,
477 gap: self.thresholds.benford_p_value_min - benford.p_value,
478 is_minimum: true,
479 config_paths: vec!["transactions.amount.benford_compliance".to_string()],
480 });
481 }
482 }
483
484 if let Some(ref amount) = evaluation.statistical.amount_distribution {
485 if amount.round_number_ratio < 0.05 {
486 gaps.push(MetricGap {
487 metric_name: "round_number_ratio".to_string(),
488 current_value: amount.round_number_ratio,
489 target_value: 0.10, gap: 0.10 - amount.round_number_ratio,
491 is_minimum: true,
492 config_paths: vec!["transactions.amount.round_number_bias".to_string()],
493 });
494 }
495 }
496
497 if let Some(ref temporal) = evaluation.statistical.temporal {
498 if temporal.pattern_correlation < self.thresholds.temporal_correlation_min {
499 gaps.push(MetricGap {
500 metric_name: "temporal_correlation".to_string(),
501 current_value: temporal.pattern_correlation,
502 target_value: self.thresholds.temporal_correlation_min,
503 gap: self.thresholds.temporal_correlation_min - temporal.pattern_correlation,
504 is_minimum: true,
505 config_paths: vec!["transactions.temporal.seasonality_strength".to_string()],
506 });
507 }
508 }
509
510 if let Some(ref ic) = evaluation.coherence.intercompany {
512 if ic.match_rate < self.thresholds.ic_match_rate_min {
513 gaps.push(MetricGap {
514 metric_name: "ic_match_rate".to_string(),
515 current_value: ic.match_rate,
516 target_value: self.thresholds.ic_match_rate_min,
517 gap: self.thresholds.ic_match_rate_min - ic.match_rate,
518 is_minimum: true,
519 config_paths: vec!["intercompany.match_precision".to_string()],
520 });
521 }
522 }
523
524 if let Some(ref doc_chain) = evaluation.coherence.document_chain {
525 let avg_completion =
526 (doc_chain.p2p_completion_rate + doc_chain.o2c_completion_rate) / 2.0;
527 if avg_completion < self.thresholds.document_chain_completion_min {
528 gaps.push(MetricGap {
529 metric_name: "doc_chain_completion".to_string(),
530 current_value: avg_completion,
531 target_value: self.thresholds.document_chain_completion_min,
532 gap: self.thresholds.document_chain_completion_min - avg_completion,
533 is_minimum: true,
534 config_paths: vec![
535 "document_flows.p2p.completion_rate".to_string(),
536 "document_flows.o2c.completion_rate".to_string(),
537 ],
538 });
539 }
540 }
541
542 if let Some(ref uniqueness) = evaluation.quality.uniqueness {
544 if uniqueness.duplicate_rate > self.thresholds.duplicate_rate_max {
545 gaps.push(MetricGap {
546 metric_name: "duplicate_rate".to_string(),
547 current_value: uniqueness.duplicate_rate,
548 target_value: self.thresholds.duplicate_rate_max,
549 gap: uniqueness.duplicate_rate - self.thresholds.duplicate_rate_max,
550 is_minimum: false, config_paths: vec!["data_quality.duplicates.exact_rate".to_string()],
552 });
553 }
554 }
555
556 if let Some(ref completeness) = evaluation.quality.completeness {
557 if completeness.overall_completeness < self.thresholds.completeness_rate_min {
558 gaps.push(MetricGap {
559 metric_name: "completeness_rate".to_string(),
560 current_value: completeness.overall_completeness,
561 target_value: self.thresholds.completeness_rate_min,
562 gap: self.thresholds.completeness_rate_min - completeness.overall_completeness,
563 is_minimum: true,
564 config_paths: vec!["data_quality.missing_values.overall_rate".to_string()],
565 });
566 }
567 }
568
569 if let Some(ref labels) = evaluation.ml_readiness.labels {
571 if labels.anomaly_rate < self.thresholds.anomaly_rate_min {
572 gaps.push(MetricGap {
573 metric_name: "anomaly_rate".to_string(),
574 current_value: labels.anomaly_rate,
575 target_value: self.thresholds.anomaly_rate_min,
576 gap: self.thresholds.anomaly_rate_min - labels.anomaly_rate,
577 is_minimum: true,
578 config_paths: vec!["anomaly_injection.base_rate".to_string()],
579 });
580 } else if labels.anomaly_rate > self.thresholds.anomaly_rate_max {
581 gaps.push(MetricGap {
582 metric_name: "anomaly_rate".to_string(),
583 current_value: labels.anomaly_rate,
584 target_value: self.thresholds.anomaly_rate_max,
585 gap: labels.anomaly_rate - self.thresholds.anomaly_rate_max,
586 is_minimum: false,
587 config_paths: vec!["anomaly_injection.base_rate".to_string()],
588 });
589 }
590
591 if labels.label_coverage < self.thresholds.label_coverage_min {
592 gaps.push(MetricGap {
593 metric_name: "label_coverage".to_string(),
594 current_value: labels.label_coverage,
595 target_value: self.thresholds.label_coverage_min,
596 gap: self.thresholds.label_coverage_min - labels.label_coverage,
597 is_minimum: true,
598 config_paths: vec!["anomaly_injection.label_all".to_string()],
599 });
600 }
601 }
602
603 if let Some(ref graph) = evaluation.ml_readiness.graph {
604 if graph.connectivity_score < self.thresholds.graph_connectivity_min {
605 gaps.push(MetricGap {
606 metric_name: "graph_connectivity".to_string(),
607 current_value: graph.connectivity_score,
608 target_value: self.thresholds.graph_connectivity_min,
609 gap: self.thresholds.graph_connectivity_min - graph.connectivity_score,
610 is_minimum: true,
611 config_paths: vec!["graph_export.ensure_connected".to_string()],
612 });
613 }
614 }
615
616 if let Some(ref hr) = evaluation.coherence.hr_payroll {
620 if hr.gross_to_net_accuracy < 0.999 {
621 gaps.push(MetricGap {
622 metric_name: "payroll_accuracy".to_string(),
623 current_value: hr.gross_to_net_accuracy,
624 target_value: 0.999,
625 gap: 0.999 - hr.gross_to_net_accuracy,
626 is_minimum: true,
627 config_paths: vec!["hr.payroll.calculation_precision".to_string()],
628 });
629 }
630 }
631
632 if let Some(ref mfg) = evaluation.coherence.manufacturing {
634 if mfg.yield_rate_consistency < 0.95 {
635 gaps.push(MetricGap {
636 metric_name: "manufacturing_yield".to_string(),
637 current_value: mfg.yield_rate_consistency,
638 target_value: 0.95,
639 gap: 0.95 - mfg.yield_rate_consistency,
640 is_minimum: true,
641 config_paths: vec!["manufacturing.production_orders.yield_target".to_string()],
642 });
643 }
644 }
645
646 if let Some(ref sourcing) = evaluation.coherence.sourcing {
648 if sourcing.rfx_completion_rate < 0.90 {
649 gaps.push(MetricGap {
650 metric_name: "s2c_chain_completion".to_string(),
651 current_value: sourcing.rfx_completion_rate,
652 target_value: 0.90,
653 gap: 0.90 - sourcing.rfx_completion_rate,
654 is_minimum: true,
655 config_paths: vec!["source_to_pay.rfx_completion_rate".to_string()],
656 });
657 }
658 }
659
660 if let Some(ref as_eval) = evaluation.ml_readiness.anomaly_scoring {
662 if as_eval.anomaly_separability < self.thresholds.min_anomaly_separability {
663 gaps.push(MetricGap {
664 metric_name: "anomaly_separability".to_string(),
665 current_value: as_eval.anomaly_separability,
666 target_value: self.thresholds.min_anomaly_separability,
667 gap: self.thresholds.min_anomaly_separability - as_eval.anomaly_separability,
668 is_minimum: true,
669 config_paths: vec!["anomaly_injection.base_rate".to_string()],
670 });
671 }
672 }
673
674 if let Some(ref fq_eval) = evaluation.ml_readiness.feature_quality {
676 if fq_eval.feature_quality_score < self.thresholds.min_feature_quality {
677 gaps.push(MetricGap {
678 metric_name: "feature_quality".to_string(),
679 current_value: fq_eval.feature_quality_score,
680 target_value: self.thresholds.min_feature_quality,
681 gap: self.thresholds.min_feature_quality - fq_eval.feature_quality_score,
682 is_minimum: true,
683 config_paths: vec!["graph_export.feature_completeness".to_string()],
684 });
685 }
686 }
687
688 if let Some(ref gnn_eval) = evaluation.ml_readiness.gnn_readiness {
690 if gnn_eval.gnn_readiness_score < self.thresholds.min_gnn_readiness {
691 gaps.push(MetricGap {
692 metric_name: "gnn_readiness".to_string(),
693 current_value: gnn_eval.gnn_readiness_score,
694 target_value: self.thresholds.min_gnn_readiness,
695 gap: self.thresholds.min_gnn_readiness - gnn_eval.gnn_readiness_score,
696 is_minimum: true,
697 config_paths: vec![
698 "graph_export.ensure_connected".to_string(),
699 "cross_process_links.enabled".to_string(),
700 ],
701 });
702 }
703 }
704
705 if let Some(ref dg_eval) = evaluation.ml_readiness.domain_gap {
707 if dg_eval.domain_gap_score > self.thresholds.max_domain_gap {
708 gaps.push(MetricGap {
709 metric_name: "domain_gap".to_string(),
710 current_value: dg_eval.domain_gap_score,
711 target_value: self.thresholds.max_domain_gap,
712 gap: dg_eval.domain_gap_score - self.thresholds.max_domain_gap,
713 is_minimum: false,
714 config_paths: vec!["distributions.industry_profile".to_string()],
715 });
716 }
717 }
718
719 gaps
720 }
721
722 fn generate_patch(
724 &self,
725 gap: &MetricGap,
726 mapping: &MetricConfigMapping,
727 ) -> Option<ConfigPatch> {
728 let suggested_value = match mapping.compute_value {
729 ComputeStrategy::EnableBoolean => "true".to_string(),
730 ComputeStrategy::SetFixed(v) => format!("{:.4}", v),
731 ComputeStrategy::IncreaseByGap => format!("{:.4}", gap.current_value + gap.gap * 1.2),
732 ComputeStrategy::DecreaseByGap => {
733 format!("{:.4}", (gap.current_value - gap.gap * 1.2).max(0.0))
734 }
735 ComputeStrategy::SetToTarget => format!("{:.4}", gap.target_value),
736 ComputeStrategy::MultiplyByGapFactor => {
737 let factor = if gap.is_minimum {
738 1.0 + gap.severity() * 0.5
739 } else {
740 1.0 / (1.0 + gap.severity() * 0.5)
741 };
742 format!("{:.4}", gap.current_value * factor)
743 }
744 };
745
746 let confidence = mapping.influence * (1.0 - gap.severity() * 0.3);
747 let impact = format!(
748 "Should improve {} from {:.3} toward {:.3}",
749 gap.metric_name, gap.current_value, gap.target_value
750 );
751
752 Some(
753 ConfigPatch::new(&mapping.config_path, suggested_value)
754 .with_current(format!("{:.4}", gap.current_value))
755 .with_confidence(confidence)
756 .with_impact(impact),
757 )
758 }
759
760 fn generate_summary(&self, result: &AutoTuneResult) -> String {
762 if result.patches.is_empty() {
763 "No configuration changes suggested. All metrics meet thresholds.".to_string()
764 } else {
765 let high_confidence: Vec<_> = result
766 .patches
767 .iter()
768 .filter(|p| p.confidence > 0.7)
769 .collect();
770 let addressable = result.addressed_metrics.len();
771 let unaddressable = result.unaddressable_metrics.len();
772
773 format!(
774 "Suggested {} configuration changes ({} high-confidence). \
775 {} metrics can be improved, {} require manual investigation.",
776 result.patches.len(),
777 high_confidence.len(),
778 addressable,
779 unaddressable
780 )
781 }
782 }
783
784 pub fn thresholds(&self) -> &EvaluationThresholds {
786 &self.thresholds
787 }
788}
789
790impl Default for AutoTuner {
791 fn default() -> Self {
792 Self::new()
793 }
794}
795
796#[cfg(test)]
797#[allow(clippy::unwrap_used)]
798mod tests {
799 use super::*;
800 use crate::statistical::{BenfordAnalysis, BenfordConformity};
801
802 #[test]
803 fn test_auto_tuner_creation() {
804 let tuner = AutoTuner::new();
805 assert!(!tuner.metric_mappings.is_empty());
806 }
807
808 #[test]
809 fn test_config_patch_builder() {
810 let patch = ConfigPatch::new("test.path", "value")
811 .with_current("old")
812 .with_confidence(0.8)
813 .with_impact("Should help");
814
815 assert_eq!(patch.path, "test.path");
816 assert_eq!(patch.current_value, Some("old".to_string()));
817 assert_eq!(patch.confidence, 0.8);
818 }
819
820 #[test]
821 fn test_auto_tune_result() {
822 let mut result = AutoTuneResult::new();
823 assert!(!result.has_patches());
824
825 result
826 .patches
827 .push(ConfigPatch::new("test", "value").with_confidence(0.9));
828 assert!(result.has_patches());
829
830 let sorted = result.patches_by_confidence();
831 assert_eq!(sorted.len(), 1);
832 }
833
834 #[test]
835 fn test_metric_gap_severity() {
836 let gap = MetricGap {
837 metric_name: "test".to_string(),
838 current_value: 0.02,
839 target_value: 0.05,
840 gap: 0.03,
841 is_minimum: true,
842 config_paths: vec![],
843 };
844
845 assert!((gap.severity() - 0.6).abs() < 0.001);
847 }
848
849 #[test]
850 fn test_analyze_empty_evaluation() {
851 let tuner = AutoTuner::new();
852 let evaluation = ComprehensiveEvaluation::new();
853
854 let result = tuner.analyze(&evaluation);
855
856 assert!(result.patches.is_empty());
858 }
859
860 #[test]
861 fn test_analyze_with_benford_gap() {
862 let tuner = AutoTuner::new();
863 let mut evaluation = ComprehensiveEvaluation::new();
864
865 evaluation.statistical.benford = Some(BenfordAnalysis {
867 sample_size: 1000,
868 observed_frequencies: [0.1; 9],
869 observed_counts: [100; 9],
870 expected_frequencies: [
871 0.301, 0.176, 0.125, 0.097, 0.079, 0.067, 0.058, 0.051, 0.046,
872 ],
873 chi_squared: 25.0,
874 degrees_of_freedom: 8,
875 p_value: 0.01, mad: 0.02,
877 conformity: BenfordConformity::NonConforming,
878 max_deviation: (1, 0.2), passes: false,
880 anti_benford_score: 0.5,
881 });
882
883 let result = tuner.analyze(&evaluation);
884
885 assert!(!result.patches.is_empty());
887 assert!(result
888 .addressed_metrics
889 .contains(&"benford_p_value".to_string()));
890 }
891}