Skip to main content

datasynth_eval/enhancement/
auto_tuner.rs

1//! Auto-tuning engine for deriving optimal configuration from evaluation results.
2//!
3//! The AutoTuner analyzes evaluation results to identify metric gaps and
4//! computes suggested configuration values that should improve those metrics.
5
6use crate::{ComprehensiveEvaluation, EvaluationThresholds};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// A configuration patch representing a change to apply.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ConfigPatch {
13    /// Configuration path (dot-separated).
14    pub path: String,
15    /// The current value (if known).
16    pub current_value: Option<String>,
17    /// The suggested new value.
18    pub suggested_value: String,
19    /// Confidence level (0.0-1.0) that this change will help.
20    pub confidence: f64,
21    /// Expected improvement description.
22    pub expected_impact: String,
23}
24
25impl ConfigPatch {
26    /// Create a new config patch.
27    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    /// Set the current value.
38    pub fn with_current(mut self, value: impl Into<String>) -> Self {
39        self.current_value = Some(value.into());
40        self
41    }
42
43    /// Set the confidence level.
44    pub fn with_confidence(mut self, confidence: f64) -> Self {
45        self.confidence = confidence.clamp(0.0, 1.0);
46        self
47    }
48
49    /// Set the expected impact.
50    pub fn with_impact(mut self, impact: impl Into<String>) -> Self {
51        self.expected_impact = impact.into();
52        self
53    }
54}
55
56/// Result of auto-tuning analysis.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct AutoTuneResult {
59    /// Configuration patches to apply.
60    pub patches: Vec<ConfigPatch>,
61    /// Overall improvement score (0.0-1.0).
62    pub expected_improvement: f64,
63    /// Metrics that will be addressed.
64    pub addressed_metrics: Vec<String>,
65    /// Metrics that cannot be automatically fixed.
66    pub unaddressable_metrics: Vec<String>,
67    /// Summary message.
68    pub summary: String,
69}
70
71impl AutoTuneResult {
72    /// Create a new empty result.
73    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    /// Check if any patches are suggested.
84    pub fn has_patches(&self) -> bool {
85        !self.patches.is_empty()
86    }
87
88    /// Get patches sorted by confidence (highest first).
89    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/// Metric gap analysis result.
107#[derive(Debug, Clone)]
108pub struct MetricGap {
109    /// Name of the metric.
110    pub metric_name: String,
111    /// Current value.
112    pub current_value: f64,
113    /// Target threshold value.
114    pub target_value: f64,
115    /// Gap (target - current for min thresholds, current - target for max).
116    pub gap: f64,
117    /// Whether this is a minimum threshold (true) or maximum (false).
118    pub is_minimum: bool,
119    /// Related configuration paths.
120    pub config_paths: Vec<String>,
121}
122
123impl MetricGap {
124    /// Calculate the severity of the gap (0.0-1.0).
125    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
138/// Auto-tuner that derives optimal configuration from evaluation results.
139pub struct AutoTuner {
140    /// Thresholds to compare against.
141    thresholds: EvaluationThresholds,
142    /// Known metric-to-config mappings.
143    metric_mappings: HashMap<String, Vec<MetricConfigMapping>>,
144}
145
146/// Mapping from a metric to configuration paths that affect it.
147#[derive(Debug, Clone)]
148struct MetricConfigMapping {
149    /// Configuration path.
150    config_path: String,
151    /// How much influence this config has on the metric (0.0-1.0).
152    influence: f64,
153    /// Function to compute suggested value given the gap.
154    compute_value: ComputeStrategy,
155}
156
157/// Strategy for computing suggested config values.
158#[derive(Debug, Clone, Copy)]
159enum ComputeStrategy {
160    /// Enable a boolean flag.
161    EnableBoolean,
162    /// Set to a specific value.
163    SetFixed(f64),
164    /// Increase by the gap amount.
165    IncreaseByGap,
166    /// Decrease by the gap amount.
167    DecreaseByGap,
168    /// Set to target value directly.
169    SetToTarget,
170}
171
172impl AutoTuner {
173    /// Create a new auto-tuner with default thresholds.
174    pub fn new() -> Self {
175        Self::with_thresholds(EvaluationThresholds::default())
176    }
177
178    /// Create an auto-tuner with specific thresholds.
179    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    /// Initialize known metric-to-config mappings.
189    fn initialize_mappings(&mut self) {
190        // Benford's Law
191        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        // Round number bias
201        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        // Temporal correlation
211        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        // Anomaly rate
221        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        // Label coverage
231        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        // Duplicate rate
241        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        // Completeness
251        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        // IC match rate
261        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        // Document chain completion
271        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        // Graph connectivity
288        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        // --- New evaluator metric mappings ---
298
299        // Payroll accuracy
300        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        // Manufacturing yield
310        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        // S2C chain completion
320        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        // Bank reconciliation balance
330        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        // Financial reporting tie-back
340        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        // AML detectability
350        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        // Process mining coverage
360        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        // Audit evidence coverage
370        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        // Anomaly separability
380        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        // Feature quality
390        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        // GNN readiness
400        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        // Domain gap
417        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), // placeholder - needs manual review
423            }],
424        );
425    }
426
427    /// Analyze evaluation results and produce auto-tune suggestions.
428    pub fn analyze(&self, evaluation: &ComprehensiveEvaluation) -> AutoTuneResult {
429        let mut result = AutoTuneResult::new();
430
431        // Identify metric gaps
432        let gaps = self.identify_gaps(evaluation);
433
434        // Generate patches for each gap
435        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        // Calculate expected improvement
451        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        // Generate summary
458        result.summary = self.generate_summary(&result);
459
460        result
461    }
462
463    /// Identify gaps between current metrics and thresholds.
464    fn identify_gaps(&self, evaluation: &ComprehensiveEvaluation) -> Vec<MetricGap> {
465        let mut gaps = Vec::new();
466
467        // Check statistical metrics
468        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, // Target 10%
487                    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        // Check coherence metrics
508        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        // Check quality metrics
540        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, // This is a maximum threshold
548                    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        // Check ML metrics
567        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        // --- New evaluator metric gaps ---
614
615        // HR/Payroll accuracy
616        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        // Manufacturing yield
630        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        // S2C chain completion
644        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        // Anomaly separability
658        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        // Feature quality
672        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        // GNN readiness
686        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        // Domain gap (max threshold - lower is better)
703        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    /// Generate a config patch for a metric gap.
720    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    /// Generate a summary message for the auto-tune result.
750    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    /// Get the thresholds being used.
774    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        // Severity = gap / target = 0.03 / 0.05 = 0.6
835        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        // Empty evaluation should produce no patches
846        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        // Set a failing Benford analysis
855        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, // Below threshold of 0.05
865            mad: 0.02,
866            conformity: BenfordConformity::NonConforming,
867            max_deviation: (1, 0.2), // Tuple of (digit_index, deviation)
868            passes: false,
869            anti_benford_score: 0.5,
870        });
871
872        let result = tuner.analyze(&evaluation);
873
874        // Should suggest enabling Benford compliance
875        assert!(!result.patches.is_empty());
876        assert!(result
877            .addressed_metrics
878            .contains(&"benford_p_value".to_string()));
879    }
880}