Skip to content

feat(evaluation-system): add eval framework with graders, runner, report - #36

Merged
cmschuetz merged 2 commits into
mainfrom
strapped/evaluation-system/D2-eval-framework
Jul 28, 2026
Merged

feat(evaluation-system): add eval framework with graders, runner, report#36
cmschuetz merged 2 commits into
mainfrom
strapped/evaluation-system/D2-eval-framework

Conversation

@cmschuetz

@cmschuetz cmschuetz commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Add the grading and orchestration layer on top of the D1 engine: a case/suite
abstraction, three metric graders (correctness / performance / cost), a
bounded-parallel runner with A/B (baseline vs candidate prompt) and
model-matrix modes, and a human + JSON report. A correctness dip that buys a
large cost or latency win is treated as a win — A/B mode reports Δcorrectness /
Δcost / Δlatency side by side so a human judges the trade. Also wires the
process integration so agents know to run the suite alongside bun test when
changing harness prompts.

Summary

New src/eval/ files: case.ts, grade.ts (schemaConforms/assert/judge,
gradeCase), runner.ts (runSuite/runAB/runMatrix, skips-with-notice when
claude is unavailable), report.ts (aligned text table + toJSON, plus a
clearly-labelled projected-cost column), and cli.ts (bun run eval, exits
non-zero only on an A/B regression past --tolerance). Adds the eval npm
script (not part of test), a CLAUDE.md guideline, and a conventions.md
subsection. All framework tests run through the D1 fakeSpawn stub.

Acceptance criteria

  • A case runs end-to-end through the D1 engine, producing correctness score, measured cost, and latency from the envelope.
  • Built-in graders schemaConforms/assert/judge each return a GradeResult; judge routes through the engine (cached/stubbable).
  • runAB reports Δcorrectness / Δcost / Δlatency / Δturns at a fixed model; runMatrix reports a per-model grid.
  • The runner executes cases in bounded parallel and skips-with-notice when claude is unavailable.
  • bun run eval is its own script (not part of bun run test); report prints a text table, --json emits machine JSON, A/B exits non-zero only on a regression past --tolerance.
  • CLAUDE.md instructs running the eval suite alongside bun test on prompt changes; conventions.md documents the suite.
  • bun run typecheck && bun run lint && bun test green; no new runtime dependency.

Stack

All deliverables target the strapped repo (linear stack, each PR based on its parent's branch).

Deliverable Branch PR
D1 — eval engine strapped/evaluation-system/D1-eval-engine #35
D2 — eval framework strapped/evaluation-system/D2-eval-framework #36
D3 — harness eval suite strapped/evaluation-system/D3-harness-eval-suite #37
D4 — verified optimizations strapped/evaluation-system/D4-verified-optimizations #38

Depends on #35

…ponse cache

Foundational layer of the prompt-eval suite (D1). Adds src/eval/ with a
single-shot Claude Code CLI (`claude -p`) substrate — never the Anthropic SDK:

- types.ts: EvalRequest/EvalResult/Envelope + injectable Spawn and Cache seams.
- engine.ts: buildArgs (pure print-mode argv: --print --output-format json
  --json-schema --model --strict-mcp-config --settings + conditional
  system-prompt/tool-policy flags; prompt on stdin), parseEnvelope (graded,
  never throws — prefers structured_output, falls back to result, shallow
  schema check), runClaude (cache-first, spawn, cache-on-success), isAvailable
  and a skip signal distinct from graded failure for an absent CLI.
- cache.ts: content-addressed ResponseCache keyed on a canonical SHA-256 over
  every output-affecting field.
- tests/helpers/fake-claude.ts + tests/eval/*: hermetic engine/cache coverage
  via the injected Spawn stub, no real claude.

Not a plugin deployable — no build.ts entry, no version bump.
…x runner, report, CLI

Grading + orchestration layer on top of the D1 `claude -p` engine (D2).

- src/eval/case.ts: EvalCase / Variant types, defineCase, toRequest lowering.
- src/eval/grade.ts: Grader/GradeResult, built-in schemaConforms/assert/judge
  (judge routes through the D1 engine, cached/stubbable), weighted gradeCase.
- src/eval/runner.ts: runCase, bounded-parallel runSuite (mapPool), runAB delta
  record (Δcorrectness/Δcost/Δlatency/Δturns), runMatrix per-model grid; skips
  with notice when the CLI is unavailable.
- src/eval/report.ts: aligned text table + JSON over all three axes, optional
  clearly-labelled projected-cost column from a static pricing table.
- src/eval/cli.ts: `bun src/eval/cli.ts` — suite loading, --ab/--matrix/--filter/
  --models/--json/--tolerance; report-only exit 0 except an A/B correctness
  regression past tolerance → exit 1; absent claude → notice + exit 0.
- package.json: `eval` script (kept OUT of `test`; test stays hermetic/offline).
- CLAUDE.md + conventions.md: run the eval suite alongside `bun test` when
  changing harness prompts (docs-only, no generated-artifact churn, no bump).
- tests/eval/framework.test.ts: graders, gradeCase, runAB/runMatrix deltas,
  bounded pool, report text/JSON, CLI exit-code semantics — all via fakeSpawn.
Comment thread src/eval/runner.ts
export function runSuite(cases: readonly EvalCase[], opts: SuiteOptions = {}): Promise<CaseResult[]> {
const jobs: { c: EvalCase; model: string }[] = []
for (const c of cases) {
const models = opts.models ?? c.models ?? [DEFAULT_MODEL]

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runSuite resolves models with opts.models ?? c.models ?? [DEFAULT_MODEL]. Because ?? only substitutes null/undefined, a case authored with models: [] yields zero jobs and is silently dropped — no error, no notice. Treat an empty array as "unset" (or reject it loudly). Surfaced by the D2 code review; flagging for the review loop.

Comment thread src/eval/runner.ts
* faster, tighter candidate reads as POSITIVE across the board; correctness is
* candidate−baseline so a regression reads as NEGATIVE (what the CLI gate keys on).
*/
export function runAB(c: EvalCase, baseline: Variant, candidate: Variant, opts: ABOptions = {}): ABResult {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runAB is single-draw (N=1 per variant). claude -p is stochastic (temperature can't be set on opus/sonnet), and the D4 report empirically showed the same prompt's cost/latency swinging sign run-to-run (input context 86k→180k tokens). A flat --tolerance over one sample can't separate signal from noise. Recommend n-samples per variant + a variance-aware band (report mean ± CI), so the correctness verdict is trustworthy even where sub-noise-floor cost deltas stay unattributable.

Comment thread src/eval/cli.ts
const results: ABResult[] = []
for (const c of cases) {
if (!c.variants) continue
results.push(runAB(c, c.variants.baseline, c.variants.candidate, { model: flags.models?.[0] ?? DEFAULT_MODEL, spawn, cache: deps.cache }))

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In --ab mode the model is flags.models?.[0] ?? DEFAULT_MODEL, which overrides a case's own c.models[0]. A case declaring models: ['claude-opus-4-8'] is silently A/B'd at haiku when --models is omitted. Fall back to the case's declared model before DEFAULT_MODEL. (D2 review finding.)

Comment thread src/eval/cli.ts
return { code, output }
}

if (import.meta.main) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import.meta.main entrypoint calls main(...) with no deps, so no ResponseCache is wired — a live bun run eval re-spends on every run even though D1 built a content-addressed cache. Wire a default file-backed ResponseCache here (with --no-cache to bypass). Flagged as a side-discovery in the D4 report.

Base automatically changed from strapped/evaluation-system/D1-eval-engine to main July 28, 2026 22:42
@cmschuetz
cmschuetz merged commit 7b46155 into main Jul 28, 2026
@cmschuetz
cmschuetz deleted the strapped/evaluation-system/D2-eval-framework branch July 28, 2026 22:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant