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