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#[allow(dead_code)] // Variants reserved for future tuning strategies
159#[derive(Debug, Clone, Copy)]
160enum ComputeStrategy {
161    /// Enable a boolean flag.
162    EnableBoolean,
163    /// Set to a specific value.
164    SetFixed(f64),
165    /// Increase by the gap amount.
166    IncreaseByGap,
167    /// Decrease by the gap amount.
168    DecreaseByGap,
169    /// Set to target value directly.
170    SetToTarget,
171    /// Multiply current by factor based on gap.
172    MultiplyByGapFactor,
173}
174
175impl AutoTuner {
176    /// Create a new auto-tuner with default thresholds.
177    pub fn new() -> Self {
178        Self::with_thresholds(EvaluationThresholds::default())
179    }
180
181    /// Create an auto-tuner with specific thresholds.
182    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    /// Initialize known metric-to-config mappings.
192    fn initialize_mappings(&mut self) {
193        // Benford's Law
194        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        // Round number bias
204        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        // Temporal correlation
214        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        // Anomaly rate
224        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        // Label coverage
234        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        // Duplicate rate
244        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        // Completeness
254        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        // IC match rate
264        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        // Document chain completion
274        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        // Graph connectivity
291        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        // --- New evaluator metric mappings ---
301
302        // Payroll accuracy
303        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        // Manufacturing yield
313        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        // S2C chain completion
323        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        // Bank reconciliation balance
333        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        // Financial reporting tie-back
343        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        // AML detectability
353        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        // Process mining coverage
363        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        // Audit evidence coverage
373        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        // Anomaly separability
383        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        // Feature quality
393        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        // GNN readiness
403        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        // Domain gap
420        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), // placeholder - needs manual review
426            }],
427        );
428    }
429
430    /// Analyze evaluation results and produce auto-tune suggestions.
431    pub fn analyze(&self, evaluation: &ComprehensiveEvaluation) -> AutoTuneResult {
432        let mut result = AutoTuneResult::new();
433
434        // Identify metric gaps
435        let gaps = self.identify_gaps(evaluation);
436
437        // Generate patches for each gap
438        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        // Calculate expected improvement
454        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        // Generate summary
461        result.summary = self.generate_summary(&result);
462
463        result
464    }
465
466    /// Identify gaps between current metrics and thresholds.
467    fn identify_gaps(&self, evaluation: &ComprehensiveEvaluation) -> Vec<MetricGap> {
468        let mut gaps = Vec::new();
469
470        // Check statistical metrics
471        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, // Target 10%
490                    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        // Check coherence metrics
511        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        // Check quality metrics
543        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, // This is a maximum threshold
551                    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        // Check ML metrics
570        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        // --- New evaluator metric gaps ---
617
618        // HR/Payroll accuracy
619        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        // Manufacturing yield
633        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        // S2C chain completion
647        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        // Anomaly separability
661        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        // Feature quality
675        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        // GNN readiness
689        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        // Domain gap (max threshold - lower is better)
706        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    /// Generate a config patch for a metric gap.
723    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    /// Generate a summary message for the auto-tune result.
761    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    /// Get the thresholds being used.
785    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        // Severity = gap / target = 0.03 / 0.05 = 0.6
846        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        // Empty evaluation should produce no patches
857        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        // Set a failing Benford analysis
866        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, // Below threshold of 0.05
876            mad: 0.02,
877            conformity: BenfordConformity::NonConforming,
878            max_deviation: (1, 0.2), // Tuple of (digit_index, deviation)
879            passes: false,
880            anti_benford_score: 0.5,
881        });
882
883        let result = tuner.analyze(&evaluation);
884
885        // Should suggest enabling Benford compliance
886        assert!(!result.patches.is_empty());
887        assert!(result
888            .addressed_metrics
889            .contains(&"benford_p_value".to_string()));
890    }
891}