Skip to main content

forge_engine/
scoring.rs

1//! Objective-driven scoring policy.
2
3use serde::{Deserialize, Serialize};
4
5/// What kind of objective the patch is trying to achieve.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum ObjectiveKind {
9    BugFix,
10    Refactor,
11    SafetyHardening,
12    Performance,
13    Exploration,
14}
15
16/// Policy for how scores are combined for a given objective.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ObjectivePolicy {
19    pub kind: ObjectiveKind,
20    /// Weight for correctness in the objective.
21    pub correctness_weight: f64,
22    /// Weight for novelty (auxiliary, explainable).
23    pub novelty_weight: f64,
24    /// Weight for stability (may be absent if trials insufficient).
25    pub stability_weight: f64,
26    /// Whether scalarization into weighted_total is allowed.
27    pub allow_scalarization: bool,
28    /// Minimum trial count for stability to be included.
29    pub min_trials_for_stability: u32,
30}
31
32impl ObjectivePolicy {
33    /// Create a policy for a bug fix objective.
34    pub fn bug_fix() -> Self {
35        Self {
36            kind: ObjectiveKind::BugFix,
37            correctness_weight: 0.85,
38            novelty_weight: 0.05,
39            stability_weight: 0.10,
40            allow_scalarization: true,
41            min_trials_for_stability: 3,
42        }
43    }
44
45    /// Create a policy for a refactoring objective.
46    pub fn refactor() -> Self {
47        Self {
48            kind: ObjectiveKind::Refactor,
49            correctness_weight: 0.70,
50            novelty_weight: 0.20,
51            stability_weight: 0.10,
52            allow_scalarization: true,
53            min_trials_for_stability: 3,
54        }
55    }
56
57    /// Create a policy for a safety hardening objective.
58    pub fn safety() -> Self {
59        Self {
60            kind: ObjectiveKind::SafetyHardening,
61            correctness_weight: 0.90,
62            novelty_weight: 0.0,
63            stability_weight: 0.10,
64            allow_scalarization: true,
65            min_trials_for_stability: 5,
66        }
67    }
68
69    /// Create a policy for a performance objective.
70    pub fn performance() -> Self {
71        Self {
72            kind: ObjectiveKind::Performance,
73            correctness_weight: 0.60,
74            novelty_weight: 0.10,
75            stability_weight: 0.30,
76            allow_scalarization: false, // timing needs statistical backing
77            min_trials_for_stability: 5,
78        }
79    }
80
81    /// Create a policy for exploratory work.
82    pub fn exploration() -> Self {
83        Self {
84            kind: ObjectiveKind::Exploration,
85            correctness_weight: 0.50,
86            novelty_weight: 0.40,
87            stability_weight: 0.10,
88            allow_scalarization: false, // exploratory, not production-grade
89            min_trials_for_stability: 1,
90        }
91    }
92
93    /// Compute weighted total if scalarization is allowed and stability is valid.
94    pub fn compute_weighted_total(
95        &self,
96        correctness: f64,
97        novelty: f64,
98        stability: Option<f64>,
99        trial_count: u32,
100    ) -> Option<f64> {
101        if !self.allow_scalarization {
102            return None;
103        }
104
105        let stability_val = if trial_count >= self.min_trials_for_stability {
106            stability.unwrap_or(0.0)
107        } else {
108            0.0 // insufficient trials, stability excluded
109        };
110
111        Some(
112            self.correctness_weight * correctness
113                + self.novelty_weight * novelty
114                + self.stability_weight * stability_val,
115        )
116    }
117}
118
119impl Default for ObjectivePolicy {
120    fn default() -> Self {
121        Self::refactor()
122    }
123}
124
125/// Check comparability classification.
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
127#[serde(rename_all = "snake_case")]
128pub enum ComparabilityClass {
129    /// Core checks whose results are directly comparable between baseline and patched.
130    ComparableCore,
131    /// Diagnostic checks only meaningful in the patched context.
132    PatchOnlyDiagnostic,
133    /// Informational-only, never feeds metrics.
134    Informational,
135}
136
137/// A classified check in an execution plan.
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct PlannedCheck {
140    pub check_kind: crate::exec::backend::CheckKind,
141    pub comparability: ComparabilityClass,
142    /// Override from task fixture config, if any.
143    pub config_override: bool,
144}
145
146/// Frozen execution plan with classified checks.
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct PatchExecutionPlan {
149    pub checks: Vec<PlannedCheck>,
150    pub frozen: bool,
151}
152
153impl PatchExecutionPlan {
154    /// Create a default Cargo-based execution plan.
155    pub fn cargo_default() -> Self {
156        Self {
157            checks: vec![
158                PlannedCheck {
159                    check_kind: crate::exec::backend::CheckKind::Fmt,
160                    comparability: ComparabilityClass::ComparableCore,
161                    config_override: false,
162                },
163                PlannedCheck {
164                    check_kind: crate::exec::backend::CheckKind::Clippy,
165                    comparability: ComparabilityClass::ComparableCore,
166                    config_override: false,
167                },
168                PlannedCheck {
169                    check_kind: crate::exec::backend::CheckKind::Test,
170                    comparability: ComparabilityClass::ComparableCore,
171                    config_override: false,
172                },
173            ],
174            frozen: true,
175        }
176    }
177
178    /// Get only ComparableCore checks.
179    pub fn comparable_checks(&self) -> Vec<&PlannedCheck> {
180        self.checks
181            .iter()
182            .filter(|c| c.comparability == ComparabilityClass::ComparableCore)
183            .collect()
184    }
185}