Uh oh!
There was an error while loading. Please reload this page.
fix(json): stop a skipped sub-score being averaged as a mismatch - #210
Conversation
Score documents that "If the score is None, the evaluation is considered to be skipped", and json_diff filters those out before averaging. The two branches then disagreed about what to divide by. The dict branch divided by len(base_scores), the count after filtering, so a skipped key was excluded from both the numerator and the denominator and correctly ignored. The list branch divided by max(len(o1), len(o2)), which still counts the skipped element, so the same skip was averaged in as a zero. The result is that one skipped comparison scores 1.0 inside an object and 0.5 inside a two-element array, for identical values and an identical scorer. The dict branch also divided by len(base_scores) with no guard. An object whose every comparison is skipped leaves that list empty and raises ZeroDivisionError rather than reporting a skip. Both branches now drop skipped comparisons from the denominator and return None, propagating the skip, when nothing is left to average. The list denominator stays max(len(o1), len(o2)) minus the skips, so elements with no counterpart are still counted as a real difference; only the skips come out. Eight tests, four of which fail on main. The other four pin what must not move: missing elements are still penalised, and unskipped lists, dicts and empty containers score exactly as before.
Flagging in case it is not visible from your side: the workflow run on this PR needs maintainer approval before anything executes, so the checks here are empty rather than failing. Locally the eight added cases pass, four of which fail on |
The red checks that showed up on this PR today are not test failures. The six runs were created on 2 Aug, sat waiting for maintainer approval, and GitHub expired them at the 30 day mark (created 2026-08-02 02:33 UTC, marked failed 2026-09-01 02:36 UTC). All six have zero jobs and no logs, so nothing actually ran. The same thing has happened to the other fork PRs on this repo, so it does not look specific to this branch. I re-checked the change locally today on Python 3.13:
The branch is still level with main, so there is nothing to rebase. Since these runs have already expired, a fresh one would need to be queued before it can be approved. I can push an empty commit to trigger that if it is useful, just say the word. |
The problem
Scoredocuments the contract:JSONDiff.json_difffilters those out before averaging, but the two branches then divide bydifferent things:
In an object a skipped key is removed from the numerator and the denominator, so it is
ignored, which matches the documented meaning. In an array it is removed from the numerator
only, so it is averaged in as a zero.
The same skip therefore lands differently depending on the container:
main{"a": "skip", "b": "same"}["skip", "same"]Same values, same scorer, same skip.
This is reachable with any scorer that can abstain, which is the case the
Nonescore existsfor: an LLM judge that declines to answer, a scorer that cannot parse a field. The array
result is silently lower and nothing raises.
Second, smaller issue
The dict branch divides by
len(base_scores)with no guard. An object whose comparisons areall skipped leaves that list empty and raises
ZeroDivisionError. The empty-object case ishandled above it, so this needs a non-empty object rather than an edge case in the inputs.
The change
Both branches now drop skipped comparisons from the denominator, and return
Nonewhennothing is left to average, which propagates the skip upward instead of inventing a number or
raising.
The list denominator stays
max(len(o1), len(o2))minus the skips. That distinction isdeliberate: an element with no counterpart is a genuine difference and must stay counted,
which is what
max(...)contributes over thezip. Only the skips come out.Tests
py/autoevals/test_json_skipped_scores.py, eight tests, four of which fail onmain:The other four pin behaviour that must not move: missing elements are still penalised
(
["a"]vs["a", "b"]is still 0.5), and unskipped lists, dicts and empty containers scoreexactly as before.
py/autoevals/test_json.py: 4 passed. Acrosspy/autoevalsthe results are identical withand without this diff (21 failed, 39 passed, 8 errors, all needing API keys or litellm).
blackandisortclean at line-length 119.