Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

.nornir/bakeoff-judge.md

Bake-off codegen judge (CodegenJudge / AnswerJudge)

Design + guide for the compile-and-test quality scorer that finally fills the bake-off's missing score. Companion to .nornir/bakeoff.md (the H5 agent × model matrix). Also doubles as the funnel #45 task-decomposition accept/reject oracle.

1. The problem it solves

A bake-off (nornir bakeoff run) records every (agent, model) cell's answer plus its economics — latency, tokens/s, cost, mcp calls — into agent_model_runs. But for a real ollama run the quality score was left 0.0 (OllamaCaller has no judge), so real leaderboards ranked cells by speed alone: the fastest gibberish beat the slow-but-correct answer. The mock/demo paths hand-author plausible scores, which never reflect whether the code works.

The codegen judge replaces that 0.0 with a score earned by actually building and testing the answer.

2. What it is

A pure-Rust evaluator — it does not call an LLM. It takes an answer that was already produced and grades it:

  1. Copy the target crate into a temp scratch dir (skipping target/ + .git/ so it's fast + clean).
  2. Apply the answer: either overwrite a file with it (the answer is a full file body), or git apply it (the answer is a unified diff/patch).
  3. cargo build the scratch crate.
  4. Run the acceptance command (default cargo test; configurable, e.g. a specific --test or a doctest run).
  5. Turn the outcome into a 0.0..=1.0 score + a structured [Verdict].

Scoring

outcome score
answer doesn't apply (bad patch / path) 0.0
applies but doesn't compile 0.0
compiles, some acceptance tests fail fraction passing (p/n)
compiles, accept cmd fails, count unknown 0.5 (compiles-but-fails)
compiles, accept cmd passes, no count 1.0
build + all acceptance tests pass 1.0

The fraction comes from summing every test result: ok. N passed; M failed; … line cargo prints (unit + integration + doc suites each emit one).

3. The API

// The trait — one method, never panics on a bad answer (a bad answer is a 0.0
// Verdict, not an Err; Err is reserved for the judge's own infra failing).
pub trait AnswerJudge {
    fn judge(&self, answer: &str) -> anyhow::Result<Verdict>;
}

pub struct Verdict {
    pub compiled: bool,
    pub tests_passed: u32,
    pub tests_total: u32,
    pub score: f64,        // 0.0..=1.0 — becomes the bake-off row's score
    pub message: String,   // one-line explanation
}
impl Verdict {
    pub fn rejected(msg) -> Verdict;   // a 0.0 "never built" verdict
    pub fn accepted(&self) -> bool;    // compiled && all tests passed  ← funnel oracle
}

// The first impl: compile-and-test.
pub struct CodegenJudge { /* holds a CodegenSpec */ }

pub struct CodegenSpec {
    pub crate_dir: PathBuf,     // crate copied into scratch
    pub apply: ApplyMode,       // WriteFile { rel_path } | ApplyPatch
    pub accept_cmd: Vec<String>,// default ["cargo","test"]
}
// constructors:
CodegenSpec::write_file(crate_dir, "src/lib.rs")
CodegenSpec::apply_patch(crate_dir)
    .with_accept_cmd(vec!["cargo".into(), "test".into(), "--test".into(), "it".into()])

AnswerJudge is the seam so the funnel epic reuses the same evaluator; CodegenJudge is the first impl, and more (lint judge, benchmark judge, …) can slot in without touching the bake-off.

4. How it wires into the bake-off

agent_model_runs::score_rows_with_judge(&mut rows, &judge):

run_bakeoff_matrix → rows (score 0.0)
└─ --judge-crate set?
   ├─[no]──────────────────────────────────────────┐
   └─[yes]─▶ score_rows_with_judge                  │
            CodegenJudge.judge(output)              │
            └─▶ row.score = verdict.score ──────────┤
                                                    ▼
                                          persist as-is
                                          └─▶ leaderboard ranks by REAL score, then tok/s

5. CLI

New flags on nornir bakeoff run:

flag meaning
--judge-crate <PATH> target crate; turns on compile-and-test scoring
--judge-file <REL_PATH> answer is a full file body that overwrites this path
--judge-patch answer is a unified diff, git apply-ed (excl. --judge-file)
--judge-cmd "<CMD>" acceptance command (default cargo test)
# Score each model's answer by building+testing it against a fixture crate.
nornir bakeoff run --task "Implement add(a,b)" \
  --agents local-llm --models llama3,mistral \
  --judge-crate ./fixtures/addcrate --judge-file src/lib.rs

# Patch-style answers, gated on one integration test.
nornir bakeoff run --task "Fix the race" --mock \
  --judge-crate ./fixtures/racey --judge-patch \
  --judge-cmd "cargo test --test concurrency"

The run prints each cell's verdict, e.g.:

⚖ judging 2 cell(s) by compile-and-test against `./fixtures/addcrate` (cargo test)
  local-llm/llama3: score 1.000 — compiled; all 2 acceptance tests passed
  local-llm/mistral: score 0.000 — did not compile: error[E0308]: mismatched types

…and the leaderboard then ranks by real score first, tok/s second.

6. Doubling as the funnel #45 oracle

The funnel decomposes an idea into sub-tasks; #45 needs to decide whether a sub-task's produced code is accepted or rejected. That is exactly AnswerJudge::judge(produced_code) → Verdict::accepted() (compiled AND all tests passed). The same CodegenSpec (crate + accept cmd) that scores a bake-off cell is the per-sub-task acceptance gate, so the funnel and the bake-off share one evaluator and one definition of "the code is correct".

7. Tests (inject-assert LAW)

src/warehouse/codegen_judge.rs tests feed real answers to a real scratch crate and assert real scores:

src/warehouse/agent_model_runs.rs — matrix_scored_by_codegen_judge_lands_real_scores: a MockCaller matrix whose cells emit a GOOD body, a NON-COMPILING body, and one absent (red) cell; run through score_rows_with_judge against a scratch crate. Asserts the rows land real, non-0.0 scores (good=1.0, non-compiling=0.0, red stays 0.0/red) and the leaderboard now ranks the GOOD cell first on its earned score.