Skip to content

Commit 528ca92

Browse files
author
Test User
committed
Merge remote-tracking branch 'gitea/task/2749-codebase-eval-check'
2 parents be46083 + b6e9195 commit 528ca92

7 files changed

Lines changed: 663 additions & 0 deletions

File tree

.github/workflows/agent-eval.yml

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
name: Agent Codebase Evaluation
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened]
6+
7+
concurrency:
8+
group: agent-eval-${{ github.ref }}
9+
cancel-in-progress: true
10+
11+
jobs:
12+
eval:
13+
name: Evaluate agent changes
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Checkout base branch (baseline)
17+
uses: actions/checkout@v4
18+
with:
19+
ref: ${{ github.base_ref }}
20+
path: baseline-repo
21+
22+
- name: Install Rust toolchain
23+
uses: dtolnay/rust-toolchain@stable
24+
25+
- name: Cache Cargo registry (baseline)
26+
uses: actions/cache@v4
27+
with:
28+
path: |
29+
~/.cargo/registry
30+
~/.cargo/git
31+
key: cargo-${{ runner.os }}-${{ hashFiles('baseline-repo/Cargo.lock') }}
32+
restore-keys: cargo-${{ runner.os }}-
33+
34+
- name: Capture baseline metrics
35+
working-directory: baseline-repo
36+
run: |
37+
mkdir -p ../.eval
38+
bash scripts/evaluate-agent.sh --mode baseline --output-dir ../.eval
39+
40+
- name: Checkout PR branch (candidate)
41+
uses: actions/checkout@v4
42+
with:
43+
path: candidate-repo
44+
45+
- name: Cache Cargo registry (candidate)
46+
uses: actions/cache@v4
47+
with:
48+
path: |
49+
~/.cargo/registry
50+
~/.cargo/git
51+
key: cargo-${{ runner.os }}-${{ hashFiles('candidate-repo/Cargo.lock') }}
52+
restore-keys: cargo-${{ runner.os }}-
53+
54+
- name: Capture candidate metrics and emit verdict
55+
working-directory: candidate-repo
56+
run: |
57+
bash scripts/evaluate-agent.sh \
58+
--mode candidate \
59+
--output-dir ../.eval \
60+
--baseline-dir ../.eval
61+
62+
- name: Upload evaluation report
63+
if: always()
64+
uses: actions/upload-artifact@v4
65+
with:
66+
name: eval-report-${{ github.event.pull_request.number }}
67+
path: .eval/verdict-report.json
68+
retention-days: 30

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "terraphim_eval_check"
3+
version.workspace = true
4+
edition.workspace = true
5+
authors.workspace = true
6+
rust-version = "1.91"
7+
description = "Deterministic codebase evaluation: Verdict Engine for AI-agent improvement assessment."
8+
license.workspace = true
9+
10+
[[bin]]
11+
name = "eval-check"
12+
path = "src/main.rs"
13+
14+
[lib]
15+
name = "terraphim_eval_check"
16+
path = "src/lib.rs"
17+
18+
[dependencies]
19+
serde = { workspace = true }
20+
serde_json = { workspace = true }
21+
thiserror = { workspace = true }
22+
clap = { version = "4", features = ["derive"] }
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
//! Verdict Engine for Terraphim codebase evaluation.
2+
//!
3+
//! Compares baseline and candidate metrics captured by `scripts/evaluate-agent.sh`
4+
//! and classifies the outcome as [`Verdict::Improved`], [`Verdict::Degraded`], or
5+
//! [`Verdict::Neutral`].
6+
7+
use serde::{Deserialize, Serialize};
8+
9+
/// Metrics captured for one codebase snapshot (baseline or candidate).
10+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
11+
pub struct Metrics {
12+
/// Number of `cargo test` failures (0 = all passing).
13+
pub test_failures: u32,
14+
/// Number of `cargo clippy` warnings emitted.
15+
pub clippy_warnings: u32,
16+
/// Number of `cargo clippy` errors emitted.
17+
pub clippy_errors: u32,
18+
/// Total tests executed.
19+
pub test_count: u32,
20+
}
21+
22+
/// Per-metric delta between baseline and candidate.
23+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
24+
pub struct Delta {
25+
pub test_failures: i64,
26+
pub clippy_warnings: i64,
27+
pub clippy_errors: i64,
28+
pub test_count: i64,
29+
}
30+
31+
impl Delta {
32+
pub fn compute(baseline: &Metrics, candidate: &Metrics) -> Self {
33+
Self {
34+
test_failures: candidate.test_failures as i64 - baseline.test_failures as i64,
35+
clippy_warnings: candidate.clippy_warnings as i64 - baseline.clippy_warnings as i64,
36+
clippy_errors: candidate.clippy_errors as i64 - baseline.clippy_errors as i64,
37+
test_count: candidate.test_count as i64 - baseline.test_count as i64,
38+
}
39+
}
40+
}
41+
42+
/// Classification of the AI-agent's net effect on the codebase.
43+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
44+
#[serde(rename_all = "PascalCase")]
45+
pub enum Verdict {
46+
/// The candidate is strictly better: no new failures, fewer warnings/errors.
47+
Improved,
48+
/// The candidate introduced regressions (new test failures or errors).
49+
Degraded,
50+
/// The change is a wash — neither clearly better nor worse.
51+
Neutral,
52+
}
53+
54+
impl std::fmt::Display for Verdict {
55+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56+
match self {
57+
Verdict::Improved => write!(f, "Improved"),
58+
Verdict::Degraded => write!(f, "Degraded"),
59+
Verdict::Neutral => write!(f, "Neutral"),
60+
}
61+
}
62+
}
63+
64+
/// Full evaluation report: inputs, deltas, and final verdict.
65+
#[derive(Debug, Clone, Serialize, Deserialize)]
66+
pub struct EvalReport {
67+
pub baseline: Metrics,
68+
pub candidate: Metrics,
69+
pub delta: Delta,
70+
pub verdict: Verdict,
71+
pub rationale: String,
72+
}
73+
74+
/// Evaluate baseline vs candidate metrics and produce a report.
75+
///
76+
/// Rules (applied in order):
77+
/// 1. New test failures or new clippy **errors** → **Degraded**.
78+
/// 2. Fewer failures, fewer warnings, and no new errors → **Improved**.
79+
/// 3. Otherwise → **Neutral**.
80+
pub fn evaluate(baseline: &Metrics, candidate: &Metrics) -> EvalReport {
81+
let delta = Delta::compute(baseline, candidate);
82+
83+
let (verdict, rationale) = classify(&delta);
84+
85+
EvalReport {
86+
baseline: baseline.clone(),
87+
candidate: candidate.clone(),
88+
delta,
89+
verdict,
90+
rationale,
91+
}
92+
}
93+
94+
fn classify(delta: &Delta) -> (Verdict, String) {
95+
// Rule 1: hard regressions.
96+
if delta.test_failures > 0 {
97+
return (
98+
Verdict::Degraded,
99+
format!(
100+
"Candidate introduced {} new test failure(s).",
101+
delta.test_failures
102+
),
103+
);
104+
}
105+
if delta.clippy_errors > 0 {
106+
return (
107+
Verdict::Degraded,
108+
format!(
109+
"Candidate introduced {} new clippy error(s).",
110+
delta.clippy_errors
111+
),
112+
);
113+
}
114+
115+
// Rule 2: clear improvement.
116+
let fewer_failures = delta.test_failures <= 0;
117+
let fewer_warnings = delta.clippy_warnings <= 0;
118+
let no_new_errors = delta.clippy_errors <= 0;
119+
let more_tests = delta.test_count >= 0;
120+
121+
if fewer_failures && fewer_warnings && no_new_errors && more_tests {
122+
// At least one metric must actually improve to call it Improved.
123+
if delta.test_failures < 0 || delta.clippy_warnings < 0 || delta.test_count > 0 {
124+
return (
125+
Verdict::Improved,
126+
format!(
127+
"Candidate reduced failures by {}, warnings by {}, added {} test(s).",
128+
-delta.test_failures, -delta.clippy_warnings, delta.test_count
129+
),
130+
);
131+
}
132+
}
133+
134+
// Rule 3: neutral.
135+
(
136+
Verdict::Neutral,
137+
format!(
138+
"Mixed signals: failures {:+}, warnings {:+}, errors {:+}.",
139+
delta.test_failures, delta.clippy_warnings, delta.clippy_errors
140+
),
141+
)
142+
}
143+
144+
#[cfg(test)]
145+
mod tests {
146+
use super::*;
147+
148+
fn baseline() -> Metrics {
149+
Metrics {
150+
test_failures: 0,
151+
clippy_warnings: 5,
152+
clippy_errors: 0,
153+
test_count: 100,
154+
}
155+
}
156+
157+
#[test]
158+
fn new_test_failure_is_degraded() {
159+
let candidate = Metrics {
160+
test_failures: 2,
161+
..baseline()
162+
};
163+
let report = evaluate(&baseline(), &candidate);
164+
assert_eq!(report.verdict, Verdict::Degraded);
165+
assert!(report.rationale.contains("test failure"));
166+
}
167+
168+
#[test]
169+
fn new_clippy_error_is_degraded() {
170+
let candidate = Metrics {
171+
clippy_errors: 1,
172+
..baseline()
173+
};
174+
let report = evaluate(&baseline(), &candidate);
175+
assert_eq!(report.verdict, Verdict::Degraded);
176+
assert!(report.rationale.contains("clippy error"));
177+
}
178+
179+
#[test]
180+
fn fewer_warnings_no_regressions_is_improved() {
181+
let candidate = Metrics {
182+
clippy_warnings: 2,
183+
test_count: 105,
184+
..baseline()
185+
};
186+
let report = evaluate(&baseline(), &candidate);
187+
assert_eq!(report.verdict, Verdict::Improved);
188+
}
189+
190+
#[test]
191+
fn identical_metrics_is_neutral() {
192+
let report = evaluate(&baseline(), &baseline());
193+
assert_eq!(report.verdict, Verdict::Neutral);
194+
}
195+
196+
#[test]
197+
fn more_warnings_but_no_failures_is_neutral() {
198+
let candidate = Metrics {
199+
clippy_warnings: 8,
200+
..baseline()
201+
};
202+
let report = evaluate(&baseline(), &candidate);
203+
assert_eq!(report.verdict, Verdict::Neutral);
204+
}
205+
206+
#[test]
207+
fn delta_compute() {
208+
let b = Metrics {
209+
test_failures: 0,
210+
clippy_warnings: 5,
211+
clippy_errors: 0,
212+
test_count: 100,
213+
};
214+
let c = Metrics {
215+
test_failures: 1,
216+
clippy_warnings: 3,
217+
clippy_errors: 1,
218+
test_count: 102,
219+
};
220+
let d = Delta::compute(&b, &c);
221+
assert_eq!(d.test_failures, 1);
222+
assert_eq!(d.clippy_warnings, -2);
223+
assert_eq!(d.clippy_errors, 1);
224+
assert_eq!(d.test_count, 2);
225+
}
226+
227+
#[test]
228+
fn verdict_display() {
229+
assert_eq!(Verdict::Improved.to_string(), "Improved");
230+
assert_eq!(Verdict::Degraded.to_string(), "Degraded");
231+
assert_eq!(Verdict::Neutral.to_string(), "Neutral");
232+
}
233+
234+
#[test]
235+
fn eval_report_serialises_to_json() {
236+
let report = evaluate(
237+
&baseline(),
238+
&Metrics {
239+
clippy_warnings: 2,
240+
test_count: 103,
241+
..baseline()
242+
},
243+
);
244+
let json = serde_json::to_string(&report).expect("serialise");
245+
assert!(json.contains("\"Improved\""));
246+
}
247+
}

0 commit comments

Comments
 (0)