Skip to content

Evaluator: survive a failed question, and keep what was already paid for - #211

Merged
adamjohnwright merged 4 commits into
mainfrom
fix/evaluator-hardening
Sep 14, 2026
Merged

adamjohnwright merged 4 commits into
mainfrom
fix/evaluator-hardening

Conversation

@adamjohnwright

Copy link
Copy Markdown
Contributor

A run is bought question by question — a rephrase call, a retrieval, a generation, then a judge pass over all of it. The report was written once, at the very end, after every model and every repeat.

So a rate limit on question 18 of 20 raised, unwound the loop, and discarded the seventeen answers already purchased. Nothing was on disk.

That is why the evaluation is unpleasant to run, and why it has not been run on the four retrieval changes that are owed one.

A failed question no longer ends the run

It is named on stderr, listed under failed_questions in the report, and the rest are scored without it.

The references are re-aligned to the questions that survived

This is the part that had to be got right. Dropping a question from the middle and leaving the reference list alone would score every later answer against the wrong reference — and that does not raise. It produces numbers, and they look like a result.

_kept filters the question list by the same indices, and a test pins the pairing.

Answers are written as they are produced

--transcript-log run.jsonl appends one JSON object per answer and flushes, so whatever was bought survives the process that bought it.

--concurrency answers several at once

Questions are independent, so this does not change what is measured. Results are placed by index, not appended as they arrive — appending would order them by completion time, which is the reference-misalignment bug by another route.

Default is 1, so nothing changes unless asked for.

Tests

Six, with fakes rather than API calls. Four fail against an append-based implementation, which is the mutation worth guarding against: ordering by arrival is plausible, passes casual inspection, and silently scores the wrong pairs.

FAILED test_answer_questions_keeps_what_succeeded
FAILED test_answer_questions_preserves_order_under_concurrency
FAILED test_answer_questions_order_holds_when_a_middle_question_fails
FAILED test_surviving_questions_line_up_with_their_references

Verification

  • ruff and ruff format clean, mypy clean, full suite 237 passed
  • One note: run ruff from .venv. The repo pins ^0.7.1; a newer ruff reports RUF100 in tests/evaluation/ for a rule this repo does not enable — the same version trap that made LNG parameterize OpenAI model #75 look broken.

Why this one first

It is the gate on everything else in the backlog. #116, #133, #144 and #153 all change what reaches the LLM, and the constitution requires a before-and-after measurement on real questions for exactly that. It is also the gating work for spec 002's D1–D3, and it is owed anyway for four retrieval changes that shipped unevaluated.

🤖 Generated with Claude Code

adamjohnwright and others added 2 commits September 14, 2026 17:07
A run is bought question by question -- a rephrase call, a retrieval, a
generation, then a judge pass over all of it. The report was written once, at
the very end, after every model and every repeat. So a rate limit on question
18 of 20 raised, unwound the loop, and discarded the seventeen answers already
purchased. Nothing was on disk.

That is the reason the evaluation is unpleasant to run, and the reason it has
not been run on the four retrieval changes that are owed one.

**A failed question no longer ends the run.** It is named on stderr, listed
under `failed_questions` in the report, and the rest are scored without it.

**The references are re-aligned to the questions that survived.** This is the
part that had to be got right: dropping a question from the middle and leaving
the reference list alone would score every later answer against the wrong
reference. That does not raise -- it produces numbers, and they look like a
result. `_kept` filters the question list by the same indices, and a test pins
the pairing.

**Answers are written as they are produced.** `--transcript-log` appends one
JSON object per answer and flushes, so whatever was bought survives the process
that bought it.

**`--concurrency` answers several questions at once.** Questions are
independent, so this does not change what is measured. Results are placed by
index rather than appended as they arrive -- appending would order them by
completion time, which is the reference-misalignment bug by another route.
Default is 1, so nothing changes unless asked.

Six tests, with fakes rather than API calls. Four of them fail against an
append-based implementation, which is the mutation worth guarding: order by
arrival is plausible, passes casual inspection, and silently scores the wrong
pairs.

Verified: ruff and ruff format clean (the repo's pinned 0.7.4 -- a newer ruff
reports RUF100 here for a rule this repo does not enable), mypy clean, full
suite 237 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
adamjohnwright and others added 2 commits September 14, 2026 17:32
CI runs `mypy` over the whole tree; I had only run it against
src/evaluation/evaluator.py, so the fakes in the new tests went unchecked and
the lint job went red on a file I had just added.

`_FakeRephrase.invoke` returned `payload["user_input"]` -- `Any` out of a bare
`dict` -- from a function annotated `-> str`. Both fakes now take
`dict[str, Any]` and say what they return.

Run `poetry run ruff check . && poetry run ruff format --check . && poetry run
mypy` before pushing; a subset is not the gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@adamjohnwright
adamjohnwright merged commit f9b52cd into main Sep 14, 2026
10 checks passed
@adamjohnwright
adamjohnwright deleted the fix/evaluator-hardening branch September 14, 2026 17:51
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