Skip to content

feat(eval): add CompileAndRun scorer (ponytail correctness.js analog) with sandbox + YAGNI-for-tests #181

Description

@Delqhi

Summary

Erstelle CompileAndRun als neuen Scorer in cmd/sin-code/internal/evalharness/scorer.go, basierend auf ponytail's correctness.js-Gate (https://github.com/DietrichGebert/ponytail/blob/main/benchmarks/README.md:69-79):

correctness.js extracts fenced code blocks and runs per-task checks (spawns Python/Node for email, debounce, CSV; structural regex for React and FastAPI). A broken one-liner that scores great on LOC will fail on correctness.

Scorer-Logik:

  1. Extrahiere Code-Block aus Output.Text
  2. Compile (z. B. go build, python -m py_compile, tsc)
  3. Führe Self-Check aus (z. B. assert, __main__-Block, kleiner Test)
  4. Score: 1.0 wenn compile + run OK, 0.0 sonst

Boundary: Self-Check ist das Minimum, nicht das Maximum. Nicht-Lazy-Score = 0.5, ohne Self-Check = 0.0 (Compile allein reicht nicht).

Motivation

ponytail's benchmarks/README.md:69-79 definiert ein Korrektheits-Gate, das verhindert, dass triviale kaputte Lösungen als "winning" gewertet werden:

A broken one-liner that scores great on LOC will fail on correctness.

Das ist exakt die Lücke, die unser aktueller Eval-Harness hat:

  • ExactMatch (evalharness/scorer.go:14-24): zu starr für Code
  • ContainsAll (evalharness/scorer.go:27-55): funktional, aber prüft nicht Ausführung
  • SuccessFlag (evalharness/scorer.go:59+): prüft nicht Korrektheit

Beispiel: Ein Eval-Case "Schreibe eine FizzBuzz-Funktion":

  • ContainsAll("fizz", "buzz") → Score 1.0, auch wenn der Code def fizzbuzz(): return "fizz" macht
  • CompileAndRun(language="python", self_check="assert fizzbuzz(15) == 'FizzBuzz'") → Score 0.0

ponytail's Benchmark-Ergebnisse (mit Korrektheits-Gate):

  • Baseline: 518 Code-Zeilen, 0.141 USD
  • Ponytail: 39 Code-Zeilen, 0.032 USD
  • -92 % Code, -77 % Kosten – und alle Lösungen sind korrekt

Ohne Korrektheits-Gate wären die Zahlen irreführend: man könnte 1-Zeilen-Code schreiben, der "fizz" enthält, aber nicht funktioniert.

Current State in SIN-Code

Bereich Datei Zeilen Was da ist
Evalharness Scorer cmd/sin-code/internal/evalharness/scorer.go 1-126 Scorer-Interface, ExactMatch, ContainsAll, SuccessFlag
Evalharness Types cmd/sin-code/internal/evalharness/types.go 1-88 EvalCase, EvalSet, Output, Subject, Result
Eval CLI cmd/sin-code/eval_cmd.go 1-311 sin-code eval run --dataset <path>
Evalharness Tests cmd/sin-code/internal/evalharness/evalharness_test.go Tests für Runner + Scorer
LLM-Judge (geplant) Issue #171 Three-arm eval mit LLM-judge

Was fehlt: Ein Scorer, der kompilierten Code ausführt und auf Korrektheit prüft.

Was ponytail genau macht

Aus https://github.com/DietrichGebert/ponytail/blob/main/benchmarks/README.md:69-79:

File Metric Behavior
loc.js loc Measurement - always passes, records line count
correctness.js correct Gate - fails if generated code doesn't work

correctness.js extracts fenced code blocks and runs per-task checks (spawns Python/Node for email, debounce, CSV; structural regex for React and FastAPI). A broken one-liner that scores great on LOC will fail on correctness.

Plus:

Note: The React countdown and FastAPI rate-limit checks are keyword/structural only (no runtime execution), so they verify plausible structure rather than full correctness. The email, debounce, and CSV checks execute the code.

Hybrid: manche Tasks haben echte Code-Execution, manche nur strukturelle Validierung.

Aus https://github.com/DietrichGebert/ponytail/blob/main/CLAUDE.md:

Trivial one-liners need no test, YAGNI applies to tests too.

Wichtig: Bei trivialem One-Liner (z. B. dict(zip(keys, values))) brauchen wir keinen Test, weil der Code selbsterklärend ist. Erst bei nicht-trivialer Logik verlangen wir einen Self-Check.

Detailed Implementation Plan

Phase 1 — Scorer-Implementation

  1. Erweitere cmd/sin-code/internal/evalharness/scorer.go um:

    // CompileAndRun prüft, ob der generierte Code kompiliert, läuft und die
    // mitgelieferten Self-Check-Assertions bestehen. Löst ponytail's
    // correctness.js-Gate in Go aus.
    type CompileAndRun struct {
        Language  string            // "go" | "python" | "javascript" | "bash"
        SelfCheck string            // Assert-Code, der nach compile läuft
        Timeout   time.Duration     // default 30s
        Binary    string            // optional: expliziter Compiler-Aufruf
        SkipTest  bool              // wenn trivial (YAGNI for tests)
    }
    func (c CompileAndRun) Score(case EvalCase, out Output) (float64, bool, string) {
        code := extractCodeBlock(out.Text)
        if code == "" {
            return 0, false, "no code block in output"
        }
        
        // Step 1: Compile
        if err := c.compile(code); err != nil {
            return 0, false, "compile failed: " + err.Error()
        }
        
        // Step 2: Run Self-Check (unless SkipTest)
        if c.SkipTest {
            return 1, true, "compile passed, trivial one-liner"
        }
        if c.SelfCheck == "" {
            return 0.5, false, "compile passed but no self-check (YAGNI for tests requires SkipTest)"
        }
        if err := c.run(code, c.SelfCheck); err != nil {
            return 0, false, "self-check failed: " + err.Error()
        }
        
        return 1, true, "compile + self-check passed"
    }
  2. Helper-Funktionen cmd/sin-code/internal/evalharness/runner_extras.go:

    func extractCodeBlock(s string) string  // erste ```...```-Section
    func (c CompileAndRun) compile(code string) error
    func (c CompileAndRun) run(code, selfCheck string) error
    • compile: spawnt go build, python -m py_compile, tsc, bash -n je nach Language
    • run: spawnt den Code + Self-Check in einer Subshell mit Timeout
    • Beide nutzen cmd/exec mit context.WithTimeout

Phase 2 — Sandbox & Safety

  1. Sandboxed Execution (cmd/sin-code/internal/sandbox/):

    • Existing Sandbox-Package nutzen (Landlock, etc.)
    • Default-Block: network, filesystem writes outside /tmp/sin-eval-*
    • Timeout strikt: --timeout 30s
    • Memory-Limit: --max-memory 256MB (per cgroup oder ulimit)
  2. Resource-Cleanup:

    • t.Cleanup löscht /tmp/sin-eval-* nach jedem Test
    • Process-Kill auf Parent-Cancel
    • Keine persistenten Side-Effects

Phase 3 — Integration in Evalharness

  1. CLI-Erweiterung in cmd/sin-code/eval_cmd.go:

  2. JSON-Schema-Erweiterung (cmd/sin-code/internal/dataset/dataset.go):

    {
      "cases": [
        {
          "id": "fizzbuzz",
          "prompt": "Write FizzBuzz in Python",
          "expected": "",
          "scorer": {
            "type": "compile_and_run",
            "language": "python",
            "self_check": "assert fizzbuzz(15) == 'FizzBuzz'",
            "skip_test": false
          }
        }
      ]
    }

Phase 4 — Benchmarks

  1. Three-Arm-Benchmark (Issue feat(eval): three-arm eval harness (baseline/terse/<skill>) with --compare and snapshot round-trip #171 erweitern):

  2. Vergleichsmatrix (Issue feat(eval): three-arm eval harness (baseline/terse/<skill>) with --compare and snapshot round-trip #171, Phase 5):

    • Output als Markdown-Tabelle, ponytail-kompatibel
    • Direkter Vergleich mit ponytail's Benchmarks (https://github.com/DietrichGebert/ponytail/blob/main/benchmarks/README.md:34-58)

Phase 5 — Tests

  1. Unit-Test scorer_compile_run_test.go:

    • Happy-Path: Python-FizzBuzz → Score 1.0
    • Compile-Error: Python-Syntax-Fehler → Score 0.0
    • Self-Check-Fail: Falsche Assertion → Score 0.0
    • Skip-Test-Mode: trivialer One-Liner → Score 1.0 ohne Self-Check
    • No-Code-Block: leere Output → Score 0.0
  2. Sandbox-Test sandbox_test.go:

    • Network-Block: Test schlägt fehl bei socket.connect
    • Filesystem-Block: Test kann nicht in ~/ schreiben
    • Timeout: Test killt nach 30s
  3. Race-Test: go test -race -count=1 ./cmd/sin-code/internal/evalharness/...

Phase 6 — Docs

  1. Erstelle docs/compile-and-run-scorer.md: Schema, Beispiele, Sandbox-Garantien.
  2. Update AGENTS.md §14.3 (Eval-Harness): füge CompileAndRun-Scorer hinzu.
  3. Update BACKLOG.md: done.
  4. Update CHANGELOG.md Unreleased-Section.

Acceptance Criteria

  • cmd/sin-code/internal/evalharness/scorer.go enthält CompileAndRun-Scorer
  • Compile-Step funktioniert für go, python, javascript, bash
  • Self-Check-Step nutzt Sandbox (internal/sandbox/)
  • Timeout strikt: 30s default, konfigurierbar
  • SkipTest-Mode für triviale One-Liner (YAGNI für Tests)
  • JSON-Schema-Erweiterung dokumentiert
  • 5-Real-Tasks-Benchmark (von ponytail übernommen) reproduzierbar
  • Direkter Vergleich mit ponytail's Benchmarks möglich
  • docs/compile-and-run-scorer.md existiert
  • Alle Tests passen mit -race
  • golangci-lint, govulncheck, gosec (SARIF) grün

Risk and Rollback

  • Risk: Sandbox umgehbar – Code könnte aus der Sandbox ausbrechen. Mitigation: Landlock (Linux), sandbox-exec (macOS), strikter Whitelist.
  • Risk: Compile dauert zu lange (z. B. Go-Build mit vielen Deps). Mitigation: Pre-Warmed-Build-Cache, kürzerer Self-Check.
  • Risk: User-Code mit Side-Effects (z. B. löscht ~/). Mitigation: Sandbox-Block für alles außer /tmp/sin-eval-*.
  • Rollback: Revert PR. Sandbox-Setup ist additiv.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions