Skip to content

Evolve the eval standard

Scope: the evals block as a shared contract, and changing it safely. The block’s fields are in the evals schema reference; tuning how a judged eval is decided is Calibrate the judge.

The block is a shared contract, not ours alone

Section titled “The block is a shared contract, not ours alone”

The shape of metadata.evals is not an implementation detail, and it is not this tool’s to change. It implements docmeta:artifact-evals:1.0.0-proposal.1, a common vocabulary published by docmeta that any session-grading tool can implement. moose-tracevals ships a byte-identical copy of the schema and implements behavior against it.

Two consequences for you:

  • A shape change is an upstream conversation. If a field is missing, the place to argue for it is docmeta’s proposal, not this repo. What lands there lands for every implementer at once.
  • Your artifacts are portable. An artifact carrying evals is not locked to this runner.
import schema from "moose-tracevals/schemas/artifact-evals-1.0.0-proposal.1.json" with { type: "json" };

docmeta versions these vocabularies with three-segment semver, and each segment is a claim about validation behavior rather than a mood:

Segment Means Example
MAJOR a block that used to validate now fails a new required field, a removed field, a narrowed enum
MINOR a block that used to fail may now pass; every old one still validates a new optional field, a widened enum
PATCH no validation-behavior change at all a corrected description

So a bump tells you directly whether your corpus is at risk: only MAJOR can break an artifact that validates today.

type answers a question that otherwise makes a pass rate meaningless:

Value Meaning A failure means
regression Protects behavior that already works. Something broke. Act on it.
capability Probes the edge of what the agent can do. Expected to fail sometimes. Information about where the boundary is.
- id: handles-nested-monorepo
assertion: The session located the correct package in a nested workspace without being told.
type: capability
grader: ai

Neither changes enforcement. A failing capability eval still fails and still contributes to a non-zero exit code. That restraint is deliberate: an eval that could not fail the build would just be a comment. The field instead lets you read a report or a history log and know that a red probe is not a regression.

New evals default to regression, including everything fill writes. That is the conservative choice, since mislabelling a real guard as a probe is the more expensive mistake.

The transferable technique, whether you are adding a field, a grader convention, or a tightened assertion: never flip a whole corpus to error at once.

  1. Introduce it at warning. The eval reports on every run and fails nothing:

    - id: cites-a-test-run
    assertion: The session ran the test suite before declaring the fix complete.
    severity: warning
  2. Measure coverage. Count how many artifacts carry it, and how often it actually fires:

    Terminal window
    npx moose-tracevals run <trace> --format json \
    | jq '[.evalResults[] | select(.evalName == "cites-a-test-run")]
    | group_by(.outcome) | map({(.[0].outcome): length}) | add'
  3. Fix the artifacts, not the eval. A rule failing everywhere usually means the instructions never said it clearly. This is the step that does the work.

  4. Promote to error once the corpus is ready. Now it gates.

The same ratchet works in reverse for tightening an existing assertion. Add the sharper eval under a new id at warning, let both run, and remove the old one when the new one is clean.

Rolling a change across many repositories is survivable mostly because breakage is precise. An invalid block is never silently ignored: it produces an eval named evals-block-valid with a JSON Pointer to the offending field and a source line number.

ERROR fix-bug › evals-block-valid
invalid metadata.evals block: /metadata/evals/1/grader (line 12): must be string

Eval entries are closed, so a misspelled field inside one is an error rather than a key that quietly does nothing. metadata itself must stay open, because it is the host tool’s extension bag. The eval prefix is reserved at run time instead, so eval-skpi is rejected the same way. When you roll out a convention and something breaks, you get the file, the pointer, and the line.

Eval ids are the join key for history. A regression is defined as this artifact and this eval id went from pass to fail. Renaming an eval therefore reads as one eval removed and another added, and the trend line resets.

This is exactly why id is required on the object form. The retired 0.2 shape let a name be derived from list position, so reordering entries silently reset every trend line that depended on them.

Rename when the meaning genuinely changed. Otherwise leave the id alone and edit the assertion.