1pub mod artifact;
18pub mod conditions;
19pub mod footprint;
20pub mod judge;
21pub mod model;
22pub mod report;
23pub mod scorers;
24pub mod suite;
25pub mod testbench;
26
27use anyhow::Result;
28
29use conditions::{Condition, DEFAULT_BUDGET_TOKENS, assemble};
30use model::{ModelRequest, ModelRunner};
31use report::{AbReport, PairRecord, ReportConfig};
32use scorers::score_task;
33use suite::EvalSuite;
34
35pub(crate) fn sha256_hex(bytes: &[u8]) -> String {
37 use sha2::{Digest, Sha256};
38 let mut hasher = Sha256::new();
39 hasher.update(bytes);
40 crate::core::agent_identity::hex_encode(&hasher.finalize())
41}
42
43pub(crate) const SYSTEM_PROMPT: &str = "You are a precise engineering assistant. Answer using only the provided CONTEXT. \
46If the context does not contain the answer, say so. Be concise and correct.";
47
48#[derive(Debug, Clone, Copy)]
50pub struct AbRunConfig {
51 pub budget_tokens: usize,
53 pub report: ReportConfig,
55}
56
57impl Default for AbRunConfig {
58 fn default() -> Self {
59 Self {
60 budget_tokens: DEFAULT_BUDGET_TOKENS,
61 report: ReportConfig::default(),
62 }
63 }
64}
65
66pub(crate) fn build_request(context: &str, prompt: &str) -> ModelRequest {
69 ModelRequest {
70 system: SYSTEM_PROMPT.to_string(),
71 user: format!("CONTEXT:\n{context}\n\nTASK:\n{prompt}"),
72 }
73}
74
75pub fn run_ab(
78 suite: &EvalSuite,
79 suite_name: &str,
80 runner: &dyn ModelRunner,
81 cfg: &AbRunConfig,
82) -> Result<AbReport> {
83 let mut records = Vec::with_capacity(suite.tasks.len());
84 for task in &suite.tasks {
85 let workspace = task.workspace_path(&suite.dir);
86
87 let base_ctx = assemble(
88 Condition::Baseline,
89 &workspace,
90 task.query(),
91 cfg.budget_tokens,
92 )?;
93 let lean_ctx = assemble(
94 Condition::LeanCtx,
95 &workspace,
96 task.query(),
97 cfg.budget_tokens,
98 )?;
99
100 let base_resp = runner.run(&build_request(&base_ctx.text, &task.prompt))?;
101 let lean_resp = runner.run(&build_request(&lean_ctx.text, &task.prompt))?;
102
103 let base_score = score_task(task, &base_resp.text, &workspace)?;
104 let lean_score = score_task(task, &lean_resp.text, &workspace)?;
105
106 records.push(PairRecord {
107 task_id: task.id.clone(),
108 domain: task.domain.label().to_string(),
109 baseline_value: base_score.value,
110 lean_ctx_value: lean_score.value,
111 baseline_passed: base_score.passed,
112 lean_ctx_passed: lean_score.passed,
113 baseline_tokens: base_ctx.tokens,
114 lean_ctx_tokens: lean_ctx.tokens,
115 baseline_context_digest: base_ctx.digest,
116 lean_ctx_context_digest: lean_ctx.digest,
117 baseline_answer_digest: base_resp.digest(),
118 lean_ctx_answer_digest: lean_resp.digest(),
119 });
120 }
121
122 Ok(AbReport::build(
123 suite_name,
124 cfg.budget_tokens,
125 runner.fingerprint().clone(),
126 records,
127 cfg.report,
128 ))
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use model::{ModelFingerprint, ModelParams, ModelResponse, RecordedRunner, Recording};
135 use std::path::PathBuf;
136
137 fn workspace(dir: &std::path::Path) {
139 std::fs::write(
140 dir.join("answer.md"),
141 "Consolidation persists artifacts to bm25, graph, knowledge and session stores.",
142 )
143 .unwrap();
144 std::fs::write(
145 dir.join("noise.md"),
146 "Completely unrelated notes about weather, cats, and lunch plans for the week.",
147 )
148 .unwrap();
149 }
150
151 #[test]
152 fn full_pipeline_runs_and_scores_deterministically() {
153 let root = tempfile::tempdir().unwrap();
154 let ws = root.path().join("corpus");
155 std::fs::create_dir_all(&ws).unwrap();
156 workspace(&ws);
157
158 let raw = r#"{"id":"t1","domain":"qa","prompt":"Which stores does consolidation persist to?","workspace":"corpus","answers":["bm25 graph knowledge session"]}"#;
159 let suite = EvalSuite::parse(raw, root.path().to_path_buf()).unwrap();
160 let task = &suite.tasks[0];
161
162 let cfg = AbRunConfig::default();
164 let base_ctx = assemble(Condition::Baseline, &ws, task.query(), cfg.budget_tokens).unwrap();
165 let lean_ctx = assemble(Condition::LeanCtx, &ws, task.query(), cfg.budget_tokens).unwrap();
166 let base_req = build_request(&base_ctx.text, &task.prompt);
167 let lean_req = build_request(&lean_ctx.text, &task.prompt);
168
169 let fp = ModelFingerprint {
170 provider: model::PROVIDER_RECORDED.into(),
171 endpoint: "test".into(),
172 params: ModelParams {
173 model: "fixture".into(),
174 ..ModelParams::default()
175 },
176 };
177 let mut rec = Recording::new(fp);
178 rec.entries
179 .insert(base_req.key(), ModelResponse::new("I don't know."));
180 rec.entries.insert(
181 lean_req.key(),
182 ModelResponse::new("bm25, graph, knowledge and session"),
183 );
184 let runner = RecordedRunner::new(rec);
185
186 let report = run_ab(&suite, "fixture-suite", &runner, &cfg).unwrap();
187 assert_eq!(report.records.len(), 1);
188 assert!(
189 report.stats.lean_ctx_mean > report.stats.baseline_mean,
190 "lean-ctx answer should outscore the baseline: {:?}",
191 report.stats
192 );
193
194 let report2 = run_ab(&suite, "fixture-suite", &runner, &cfg).unwrap();
196 assert_eq!(
197 artifact::determinism_digest(&report),
198 artifact::determinism_digest(&report2)
199 );
200 }
201
202 #[test]
203 fn run_ab_propagates_recorded_miss() {
204 let root = tempfile::tempdir().unwrap();
205 let ws = root.path().join("corpus");
206 std::fs::create_dir_all(&ws).unwrap();
207 workspace(&ws);
208 let raw = r#"{"id":"t1","domain":"qa","prompt":"q","workspace":"corpus","answers":["x"]}"#;
209 let suite = EvalSuite::parse(raw, root.path().to_path_buf()).unwrap();
210
211 let fp = ModelFingerprint {
212 provider: model::PROVIDER_RECORDED.into(),
213 endpoint: "test".into(),
214 params: ModelParams::default(),
215 };
216 let runner = RecordedRunner::new(Recording::new(fp));
217 assert!(run_ab(&suite, "s", &runner, &AbRunConfig::default()).is_err());
219 let _ = PathBuf::new();
220 }
221}
222
223#[cfg(test)]
224mod accuracy_suite_tests {
225 use super::*;
230 use std::path::Path;
231 use suite::Domain;
232
233 fn load_accuracy_suite() -> EvalSuite {
234 let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("eval/accuracy-suite.ndjson");
235 EvalSuite::load(&path).expect("committed accuracy suite must load + validate")
236 }
237
238 #[test]
240 fn accuracy_suite_has_all_three_shapes() {
241 let suite = load_accuracy_suite();
242 let qa = suite
243 .tasks
244 .iter()
245 .filter(|t| t.domain == Domain::Qa)
246 .count();
247 let code = suite
248 .tasks
249 .iter()
250 .filter(|t| t.domain == Domain::Code)
251 .count();
252 assert!(qa >= 2, "need needle + long-context QA, got {qa}");
253 assert!(code >= 1, "need a code-edit task, got {code}");
254 }
255
256 #[test]
262 fn lean_ctx_compression_preserves_every_qa_answer() {
263 let suite = load_accuracy_suite();
264 let budget = AbRunConfig::default().budget_tokens;
265 let qa = suite.tasks.iter().filter(|t| t.domain == Domain::Qa);
266 for task in qa {
267 let ws = task.workspace_path(&suite.dir);
268 let ctx = assemble(Condition::LeanCtx, &ws, task.query(), budget)
269 .unwrap_or_else(|e| panic!("assemble {}: {e:#}", task.id));
270 let score = score_task(task, &ctx.text, &ws)
271 .unwrap_or_else(|e| panic!("score {}: {e:#}", task.id));
272 assert!(
273 score.passed,
274 "lean-ctx compression dropped the answer for '{}' ({})",
275 task.id, score.detail
276 );
277 }
278 }
279
280 #[test]
285 fn json_crush_condition_preserves_answer_and_beats_baseline() {
286 let suite = load_accuracy_suite();
287 let budget = AbRunConfig::default().budget_tokens;
288 let task = suite
289 .tasks
290 .iter()
291 .find(|t| t.id == "jsonqa-operator-clearance")
292 .expect("json-qa fixture present");
293 let ws = task.workspace_path(&suite.dir);
294
295 let crushed = assemble(Condition::JsonCrush, &ws, task.query(), budget)
296 .unwrap_or_else(|e| panic!("assemble json_crush: {e:#}"));
297 let baseline = assemble(Condition::Baseline, &ws, task.query(), budget)
298 .unwrap_or_else(|e| panic!("assemble baseline: {e:#}"));
299
300 let score = score_task(task, &crushed.text, &ws)
301 .unwrap_or_else(|e| panic!("score {}: {e:#}", task.id));
302 assert!(
303 score.passed,
304 "json_crush dropped the answer for '{}' ({})",
305 task.id, score.detail
306 );
307 assert!(
308 crushed.tokens < baseline.tokens,
309 "json_crush ({}) must beat the raw baseline ({}) on a redundant array",
310 crushed.tokens,
311 baseline.tokens
312 );
313 }
314
315 #[test]
319 fn code_task_is_solvable_and_stub_fails() {
320 let suite = load_accuracy_suite();
321 let task = suite
322 .tasks
323 .iter()
324 .find(|t| t.domain == Domain::Code)
325 .expect("code task present");
326 let ws = task.workspace_path(&suite.dir);
327
328 let reference = "factorial() { n=$1; [ \"$n\" -le 1 ] && { echo 1; return; }; \
329 r=1; i=2; while [ \"$i\" -le \"$n\" ]; do r=$((r * i)); i=$((i + 1)); done; echo \"$r\"; }";
330 let good = score_task(task, reference, &ws).unwrap();
331 assert!(good.passed, "reference solution must pass: {}", good.detail);
332
333 let stub = "factorial() { echo 0; }";
334 let bad = score_task(task, stub, &ws).unwrap();
335 assert!(!bad.passed, "wrong solution must fail the unit test");
336 }
337}