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