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 numeric value (used when the target is a known constant).
163    #[allow(dead_code)]
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}
172
173impl AutoTuner {
174    /// Create a new auto-tuner with default thresholds.
175    pub fn new() -> Self {
176        Self::with_thresholds(EvaluationThresholds::default())
177    }
178
179    /// Create an auto-tuner with specific thresholds.
180    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    /// Initialize known metric-to-config mappings.
190    fn initialize_mappings(&mut self) {
191        // Benford's Law
192        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        // Round number bias
202        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        // Temporal correlation
212        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        // Anomaly rate
222        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        // Label coverage
232        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        // Duplicate rate
242        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        // Completeness
252        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        // IC match rate
262        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        // Document chain completion
272        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        // Graph connectivity
289        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        // --- New evaluator metric mappings ---
299
300        // Payroll accuracy
301        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        // Manufacturing yield
311        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        // S2C chain completion
321        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        // Bank reconciliation balance
331        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        // Financial reporting tie-back
341        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        // AML detectability
351        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        // Process mining coverage
361        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        // Audit evidence coverage
371        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        // Anomaly separability
381        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        // Feature quality
391        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        // GNN readiness
401        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        // Domain gap: `industry_profile` is a string enum (e.g. "retail", "manufacturing"),
418        // not a continuous numeric value, so no single numeric patch can close the gap.
419        // `SetToTarget` is used here as a signal to the patch generator that the tuning
420        // direction is "raise toward target"; the human operator must then select the
421        // appropriate industry profile string that best matches the target domain.
422        // The `influence` weight (0.7) communicates medium-high confidence that changing
423        // the profile will reduce the observed domain-gap score.
424        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    /// Analyze evaluation results and produce auto-tune suggestions.
435    pub fn analyze(&self, evaluation: &ComprehensiveEvaluation) -> AutoTuneResult {
436        let mut result = AutoTuneResult::new();
437
438        // Identify metric gaps
439        let gaps = self.identify_gaps(evaluation);
440
441        // Generate patches for each gap
442        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        // Calculate expected improvement
458        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        // Generate summary
465        result.summary = self.generate_summary(&result);
466
467        result
468    }
469
470    /// Identify gaps between current metrics and thresholds.
471    fn identify_gaps(&self, evaluation: &ComprehensiveEvaluation) -> Vec<MetricGap> {
472        let mut gaps = Vec::new();
473
474        // Check statistical metrics
475        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, // Target 10%
494                    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        // Check coherence metrics
515        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        // Check quality metrics
547        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, // This is a maximum threshold
555                    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        // Check ML metrics
574        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        // --- New evaluator metric gaps ---
621
622        // HR/Payroll accuracy
623        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        // Manufacturing yield
637        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        // S2C chain completion
651        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        // Anomaly separability
665        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        // Feature quality
679        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        // GNN readiness
693        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        // Domain gap (max threshold - lower is better)
710        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    /// Generate a config patch for a metric gap.
727    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    /// Generate a summary message for the auto-tune result.
757    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    /// Get the thresholds being used.
781    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        // Severity = gap / target = 0.03 / 0.05 = 0.6
842        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        // Empty evaluation should produce no patches
853        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        // Set a failing Benford analysis
862        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, // Below threshold of 0.05
872            mad: 0.02,
873            conformity: BenfordConformity::NonConforming,
874            max_deviation: (1, 0.2), // Tuple of (digit_index, deviation)
875            passes: false,
876            anti_benford_score: 0.5,
877        });
878
879        let result = tuner.analyze(&evaluation);
880
881        // Should suggest enabling Benford compliance
882        assert!(!result.patches.is_empty());
883        assert!(result
884            .addressed_metrics
885            .contains(&"benford_p_value".to_string()));
886    }
887}