Skip to content

Feat: add unified vera judge command - #195

Draft
luca-belli wants to merge 2 commits into
refactor/judge-runtime-entrypointfrom
feat/vera-judge
Draft

Feat: add unified vera judge command#195
luca-belli wants to merge 2 commits into
refactor/judge-runtime-entrypointfrom
feat/vera-judge

Conversation

@luca-belli

Copy link
Copy Markdown
Collaborator

Summary

Adds the second unified CLI feature: vera judge.

Stacked on #194, which exposed the resolved-value judging entry point this
calls. Design decisions it implements were settled in #193.

Review guide

vera_cli/judge.py is the same five-step spine as vera_cli/generate.py:
registerrunresolve_configs_from_cli/_from_config_execute.
Nothing below step 3 reads an argparse.Namespace; nothing above it touches the
judging domain.

uv run python vera.py judge -j gpt-4o:2 --conversations output/p_*__run --target SI
uv run python vera.py judge -j gpt-4o:2 --conversations output/p_*__run --rubric SI

Both resolve to byte-identical configs, exactly as --target/--personas do for
generate.

resolve_input was reused unchanged. The per-command parameterization of the
config-or-flags rule needed no modification to serve a second command, and
ResolvedTarget's rubric fields — validated since #191, unused until now —
needed no new manifest code.

Three things that were not a mirror

RunConfig is multi-section. generation and judging are both optional,
at least one required. to_dict omits absent sections rather than emitting
null: its output doubles as input config, and each command rejects top-level keys
it does not own, so a null section would make generate --print emit something
generate itself refuses. Covered by two tests, including a round-trip through
VERA_RUN_CONFIG.

generation_persona_setstargets_from_config. It now owns only the two
rules both commands share — the target: "all" fan-out and target/explicit-field
mutual exclusion. Projecting a resolved target onto each command's fields stays
in the command module, avoiding two near-copies of the exclusivity logic.

Provider parameters return to the CLI. --judge-params, plus
--user-params and --chatbot-params for generate, which silently lost the
capability in #191
ModelSpec.from_shorthand always produced empty params,
leaving --config the only route. CHANGELOG v1.2 documents -jep in its
published migration instructions, so this was a live regression. Parameters are
supplied per role, as the legacy scripts accepted, and stored per model, so
--print shows what each model will actually use.

Judge-specific behavior (per #193)

  • --target all rejected until Phase 4 adds evaluations/<target>/; until
    then N rubric runs share one output folder, distinguishable only by timestamp.
  • --conversations takes exactly one folder — judge separately, combine with
    vera pool. List-shaped per AD-20.
  • No --resume, no single-conversation mode. Both stay in legacy judge.py.
  • --sample is the shared debug cap — conversations for judge, personas per
    file for generate — so InvocationConfig stays uniform rather than growing a
    per-command cap.
  • No -c. Judging is decoupled from chatbot selection, and -c meant
    --conversation in legacy judge.py.

Two ceilings found in the domain

JudgingConfig rejects inputs run_judging cannot honor, rather than accepting
them and silently doing something else:

  • Repeated judge model names — the domain keys models by name, so duplicates
    would collapse.
  • Differing per-model provider parametersrun_judging takes one parameter
    dict for the whole run. The schema is already shaped for per-model params, so
    this check can be deleted once the domain supports them.

Breaking change

vera judge drops legacy judge.py's fallback of writing evaluations to
evaluations/ relative to the working directory when the input is a flat
transcript folder rather than a generation run. It errors and requires
-o/--output.

The default for a generation run is unchanged — <conversation run>/evaluations/,
beside the transcripts. Old flat folders stay readable with an explicit -o,
satisfying the read-old-data guarantee in architecture.md. Dropped because the
fallback detached evaluations from the conversations that produced them, and
because a bare relative path resolves against the working directory on the CLI
but against the repository root in a config.

Recorded in CHANGELOG.md under [Unreleased] → Breaking / migration and as an
Acknowledged compatibility break in architecture.md's Phase 1 row.

Validation

  • Full non-live suite: 1,043 passed (up from 1,023), 8 deselected
  • Coverage: 75.41% (gate: 30%)
  • Ruff: no new findings (5 pre-existing E501s in distribute_files.py and
    judge/response_models.py, verified against a stashed baseline)
  • Pyright on vera.py, vera_cli/, utils/config_schema.py, judge/run.py,
    judge.py: 0 errors
  • Manually confirmed: --target/--rubric parity, --target all rejected, flat
    folder errors without -o and works with it, judge config round-trips, and
    generate's round-trip is still byte-identical with no judging key — the
    regression risk the multi-section schema introduced

🤖 Generated with Claude Code

luca-belli and others added 2 commits August 13, 2026 17:27
Mirrors `vera generate`: same five-step spine (register, run,
resolve_configs, _from_cli/_from_config, _execute), and `resolve_input` is
reused unchanged -- the per-command parameterization of the config-or-flags
rule needed no modification to serve a second command. `ResolvedTarget`'s
rubric fields, validated since #191 and unused until now, needed no new
manifest code.

Three changes were not a mirror.

`RunConfig` becomes multi-section: `generation` and `judging` are both
optional with at least one required. `to_dict` omits absent sections rather
than emitting null, because its output doubles as input config and each
command rejects top-level keys it does not own -- a null section would make
`generate --print` emit something `generate` itself refuses. The existing
byte-identical generate round-trip is covered by a test.

`generation_persona_sets` becomes `targets_from_config`, owning only the two
rules both commands share: the `target: "all"` fan-out and the
target/explicit-fields mutual exclusion. Projecting a resolved target onto
the fields a command needs stays in the command module, which avoids two
near-copies of the exclusivity logic.

Provider parameters return to the CLI as `--judge-params`, plus
`--user-params` and `--chatbot-params` for generate, which silently lost the
capability in #191 -- `ModelSpec.from_shorthand` always produced empty
params, leaving `--config` the only way to set them. CHANGELOG v1.2
documents `-jep` in its migration instructions, so this was a live
regression. Parameters are supplied per role, as the legacy scripts
accepted, and stored per model, so `--print` shows what each model will use.

Judge-specific behavior, per the decisions in #193:

- `--target all` is rejected until Phase 4 adds `evaluations/<target>/`;
  until then N rubric runs would share one output folder and be
  distinguishable only by timestamp.
- `--conversations` takes exactly one folder; judge separately and combine
  with `vera pool`.
- No `--resume` and no single-conversation mode; both stay in legacy
  `judge.py`.
- `--sample` doubles as the debug cap, limiting conversations for judge and
  personas per file for generate, so `InvocationConfig` stays uniform.

`JudgingConfig` rejects two inputs the domain cannot honor: repeated model
names, which would silently collapse because judge models are keyed by name,
and differing per-model provider parameters, because `run_judging` takes one
parameter dict for the whole run.

Breaking change, noted in CHANGELOG.md and architecture.md: `vera judge`
drops legacy `judge.py`'s fallback of writing evaluations to `evaluations/`
relative to the working directory when the input is a flat transcript folder
rather than a generation run. It errors and requires `-o/--output`. Old flat
folders remain readable with an explicit `-o`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Twenty tests mirroring tests/unit/test_vera_cli.py, focused on the decisions
that are easy to regress rather than on restating the parser.

Behavior parity: `--target` and `--rubric` resolve to identical configs, and
output defaults beside the conversation run.

The judge-specific rejections each get a test, since every one of them exists
to prevent silently wrong output: `--target all`, more than one conversations
folder, repeated judge model names, differing per-model provider parameters,
and a flat folder with no `-o` -- plus the matching case showing an explicit
`-o` still works against a flat folder.

Two tests guard the multi-section schema. One asserts a judging run omits the
`generation` key entirely rather than emitting null, since `to_dict` output
doubles as input config. The other round-trips a resolved run through
`VERA_RUN_CONFIG` and compares against the original object.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@luca-belli
luca-belli requested a review from a team as a code owner August 14, 2026 00:30
@luca-belli
luca-belli marked this pull request as draft August 14, 2026 00:31
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