forge_engine/lab/
evaluate.rs1use std::collections::BTreeSet;
2
3use crate::config::ForgeConfig;
4use crate::error::ForgeResult;
5use crate::exec::backend::CheckResult;
6use crate::lab::suite::{EvalTask, TaskExpected};
7use crate::runtime::novelty;
8use crate::runtime::patch::types::StructuredPatch;
9use crate::store::ForgeStore;
10
11#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
13pub struct ScoreVector {
14 pub correctness: f64,
15 pub novelty: f64,
16 pub stability: f64,
17 pub weighted_total: f64,
18 pub cea_confidence: Option<f64>,
19 pub cea_predicted_correctness: Option<f64>,
20}
21
22#[derive(Debug)]
24pub struct EvalRunResult {
25 pub eval_id: String,
26 pub candidate_id: String,
27 pub task_id: String,
28 pub scores: ScoreVector,
29 pub check_result: Option<CheckResult>,
30 pub patch_hash: String,
31 pub structural_sig: String,
32 pub mindstate_hash: String,
33}
34
35pub fn compute_correctness(check: &CheckResult, expected: &TaskExpected) -> f64 {
43 let mut score = 0.0_f64;
44 if !expected.require_fmt || check.fmt_pass {
45 score += 0.10;
46 }
47 if !expected.require_clippy || check.clippy_pass {
48 score += 0.30;
49 }
50 if !expected.require_tests || check.test_pass {
51 score += 0.60;
52 }
53 score }
55
56pub fn compute_novelty_score(
60 patch: &StructuredPatch,
61 store: &ForgeStore,
62 question_sig: &str,
63 config: &ForgeConfig,
64) -> ForgeResult<f64> {
65 let tags = novelty::extract_strategy_tags(patch);
66
67 let traces = store.get_recent_traces_for_question(
69 question_sig,
70 config.novelty.min_traces_for_orthogonality,
71 )?;
72
73 let recent_tags_union: BTreeSet<String> = traces
74 .iter()
75 .flat_map(|t| {
76 serde_json::from_str::<Vec<String>>(&t.strategy_tags_json).unwrap_or_default()
77 })
78 .collect();
79
80 let tag_novelty = novelty::compute_tag_novelty(&tags, &recent_tags_union);
81
82 if !traces.is_empty() {
85 let last = &traces[0];
86 let last_tags: Vec<String> =
87 serde_json::from_str(&last.strategy_tags_json).unwrap_or_default();
88
89 if tags == last_tags {
90 let current_patch_sig = {
92 let mut h = blake3::Hasher::new();
93 for edit in &patch.edits {
94 h.update(edit.path.to_string_lossy().as_bytes());
95 h.update(&(edit.ops.len() as u64).to_le_bytes());
96 }
97 h.finalize().to_hex().to_string()
98 };
99 if current_patch_sig == last.structural_sig {
101 return Ok(0.0);
102 }
103 }
104 }
105
106 Ok(tag_novelty)
107}
108
109pub fn compute_scores(
115 check: &CheckResult,
116 patch: &StructuredPatch,
117 task: &EvalTask,
118 store: &ForgeStore,
119 question_sig: &str,
120 config: &ForgeConfig,
121) -> ForgeResult<ScoreVector> {
122 let correctness = compute_correctness(check, &task.expected);
123 let novelty_raw = compute_novelty_score(patch, store, question_sig, config).unwrap_or(0.0);
124 let stability = 0.0_f64; let w = &task.weights;
127 let weighted_total =
128 w.correctness * correctness + w.novelty * novelty_raw + w.stability * stability;
129
130 Ok(ScoreVector {
131 correctness,
132 novelty: novelty_raw,
133 stability,
134 weighted_total,
135 cea_confidence: None,
136 cea_predicted_correctness: None,
137 })
138}
139
140pub fn persist_eval_run(
142 store: &ForgeStore,
143 result: &EvalRunResult,
144 backend: &str,
145 violations_json: &str,
146 logs_ref: &str,
147 cea_run_hash: Option<&str>,
148) -> ForgeResult<()> {
149 let scores_json = serde_json::to_string(&result.scores)?;
150
151 store.insert_eval_run(
152 &result.eval_id,
153 &result.candidate_id,
154 &result.task_id,
155 backend,
156 0, &result.mindstate_hash,
158 &result.patch_hash,
159 &result.structural_sig,
160 &scores_json,
161 violations_json,
162 logs_ref,
163 cea_run_hash,
164 )
165}