Repository navigation
fix(judge): read a qualified or negated figure verdict - #67
Conversation
The wrong-figure cap reads each `numeric_claims` entry's verdict with `Judge._VERDICT`: the first right/wrong word after `:`, ` -` or `(`. On 6,424 entries from real Haiku judgements, 429 were unread; most rightly (no verdict: "plausible but unverified"), but `misleading/wrong`, `ambiguous/wrong`, `speculative/wrong` — a qualifier list ending in the verdict — were too, so a figure the judge called wrong left correctness uncapped. `/` and `,` now introduce a verdict as well, and `not` flips it. Re-reading the same 6,424 entries: 13 change, 6 to wrong (all `<qualifier>/wrong`) and 7 to right (`conditional/right`, `reasonable, not wrong`, `..., correct`), none moves between right and wrong. A verdict word with no separator in front stays unread: `correctly marked as WRONG to claim` is the judge saying the answer flagged a bad claim. The panel uses the same parser. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
vchamarthi
left a comment
There was a problem hiding this comment.
Thanks, this is a nice targeted fix. The parametrized cases come straight from real Haiku output, and the must-not-cap cases (correctly marked as WRONG) pin down the boundary well. One thing on the new separators and a small comment tidy-up inline.
| #: figure is wrong. | ||
| _VERDICT = re.compile( | ||
| r"(?::|\s[-\u2013\u2014]|\()\s*(?:[a-z]+ly\s+)?(wrong|incorrect|right|correct)\b", re.I | ||
| r"(?::|\s[-\u2013\u2014]|\(|/|,)\s*(not\s+)?(?:[a-z]+ly\s+)?(wrong|incorrect|right|correct)\b", |
There was a problem hiding this comment.
Since / and , now count as separators anywhere in the string, a figure that itself contains one can win the leftmost match: left/right shift by 3: wrong and 4 KiB, correct alignment assumed: wrong both come out as right now, so the cap gets skipped where main capped them. Could we anchor the search after the first :/ -/( (or try the old pattern first and only fall back to / and ,)? A test for one of those would lock it in.
There was a problem hiding this comment.
The old _VERDICT is tried first so nothing main could read changes. The fallback only searches after the first separator. The cases you mentioned (left/right shift by 3: wrong, 4 KiB, correct alignment assumed: wrong) are pinned at test_eval_judge.py:279-280 and remain capped at 40. fa1d016 tries the old _VERDICT first, so these keep main's reading; e5b0dff additionally requires the fallback's verdict word to end the claim.
There was a problem hiding this comment.
No change on this branch: fa1d016 tries the old _VERDICT first and the fallback only searches after the first separator, so the left/right shift and 4 KiB claims stay capped at 40 (pinned at test_eval_judge.py:278-279).
| @@ -466,8 +466,18 @@ def _evaluate_answer_pair( | |||
|
|
|||
| #: A verdict after a colon, a dash or an opening parenthesis, optionally | |||
There was a problem hiding this comment.
Nit: the old two-line comment just above still describes the colon/dash/paren-only version, so there are now two descriptions of _VERDICT back to back. Worth folding it into the new one?
There was a problem hiding this comment.
Folded the duplicate comment into the updated _QUALIFIED_VERDICT documentation. The comment now explains that the search starts at the first separator and the verdict word must end the claim. Commit e5b0dff.
There was a problem hiding this comment.
No further change: the duplicate description was folded into the _QUALIFIED_VERDICT comment in e5b0dff.
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #67 +/- ##
==========================================
+ Coverage 90.06% 90.31% +0.24%
==========================================
Files 205 206 +1
Lines 24193 24432 +239
Branches 3892 3944 +52
==========================================
+ Hits 21790 22066 +276
+ Misses 1724 1699 -25
+ Partials 679 667 -12
🚀 New features to boost your workflow:
|
The loosened _VERDICT searched the whole claim, so a separator inside
the figure won the leftmost match and a wrong figure passed as right:
left/right shift by 3: wrong main: wrong PR: right
4 KiB, correct alignment assumed: wrong main: wrong PR: right
TILE 64 (not wrong): wrong main: wrong PR: right
note, wrong figure: right main: right PR: wrong
Try the old pattern first, so every claim main read keeps its reading,
and only for the rest search with "/" and "," in the verdict part (after
the first ":", " -" or "("). "not entirely wrong" and "not necessarily
right" are hedges: they stay unread (visible in unread_claims) instead
of reading as right. Folds the duplicated _VERDICT comment.
Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
|
The qualifier-list reading is right. One regression, matching the open review: Pushed fa1d016: the old pattern is tried first, so nothing main could read changes; only for the rest |
antonwolfy
left a comment
There was a problem hiding this comment.
Review at fa1d016. The fix does what it says for the qualifier lists, and trying the old _VERDICT first means nothing main could read changes: over a 900-string corpus, including dict claims, there are 0 right/wrong flips and 0 claims that capped on main and no longer cap. The problem is the fallback. It starts at the first separator in the claim and then accepts a , or / anywhere after it, so a figure or a clause of prose that contains a separator can still supply the "verdict". Two inline comments on that; I'd fix the first before merge.
What I ran, on a Linux review host at fa1d016:
make ci-local: 18 passed, 1 failed, 4 skipped. 4,318 tests passed and 1 skipped; ruff and mypy are clean; changed-line coverage is 100%. The one failure isschema_compat, which reportstool_preference.v1.jsonandtool_preference_groups.v1.jsonas removed. The PR does not touch them: the branch predates main's commit that added them. It will clear on a rebase.- Old vs new
_is_wrong_figureon the PR's examples and on the cases in the inline comments, with the LLM transport mocked. - A mutation run on the new code: 44 mutants, 17 of which pass the whole of
test_eval_judge.py. Details in the last inline comment. - The new tests against main's
judge.py: 8 fail (the three.../wrongcaps,8 ms: not right, the fouris_readcases), so those pin the fix. The "must not cap" and "figure with/or," groups already pass on main and only guard against regression.
Other points
not rightandnot correctnow cap correctness at 40, and the description does not say so.8 ms: not rightwas unread on main and is now wrong (test_eval_judge.py:285pins it). That is a reasonable reading, but the Summary lists onlynot wrongflipping to right. Please add it to the description and to the "unread -> wrong" row of the evidence table, since that table says every such change is a<qualifier>/wrongentry.- The Summary diff block is stale against fa1d016. It shows
/and,added to_VERDICT's separators. The code now keeps_VERDICTas it was and searches/and,only in a second pattern, after the first separator.
Checked, no issues
- Old readings kept:
_VERDICTruns first, sox: not wrong - wrong,x: wrong -> rightandx: incorrect/not wrongread as on main. - Hedges:
not entirely right,not really correct,neither right nor wrongandwrong, but right in contextstay as on main. - Dict claims:
{"verdict": "misleading/wrong"}is wrong,{"verdict": "reasonable, not wrong"}is right. - ReDoS: inputs up to 1.2 MB of letter runs, spaces and repeated
:a,(,-finish in under 0.2 s. - Repo rules: no schema change, no generated files, tests offline, no doc describes the verdict grammar.
methodology.mdstill matches the cap andunread_claims.
The fallback regex for qualified verdicts now anchors to the end of the claim, preventing figures or prose with separators from supplying false verdicts. This fixes cases like 'ratio 3:1, correct alignment assumed: misleading/wrong' which now correctly reads the actual verdict at the end rather than matching separators within the figure. Cases that must stay unread, like 'TILE (default, wrong on ARM): unverified', now correctly stay unread because text follows the verdict word. Removed re.MULTILINE flag to prevent $ from matching at line breaks, which would cause multi-line claims with text on the second line to match incorrectly. Added comprehensive test coverage for: - Figures containing separators (: / , - —) - Case-insensitive verdict matching (UPPERCASE) - Adverbs and negation forms (not correct, not incorrect, partially wrong) - Dict claims with qualified verdicts - Claims with no separator - Multi-line claims - False positive regression tests from the end-anchor requirement Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
antonwolfy
left a comment
There was a problem hiding this comment.
Review at e5b0dff. The end-anchor fixes what I raised at fa1d016: every claim from my two comments now reads as it should, and nothing main could read changes. One thing is worth fixing before merge: the anchored fallback still reads a trailing token as the verdict in a few cases (inline).
What I ran, on a Linux review host at e5b0dff:
make ci-local: 19 passed, 0 failed, 4 skipped. 4,345 tests passed and 1 skipped; ruff and mypy are clean; changed-line coverage is 100% (13 lines). Theschema_compatfailure from fa1d016 is gone.- Old vs new
_is_wrong_figureover about 390,000 generated claims (separators in the figure, trailing punctuation and notes, dashes, dict claims), with the LLM transport mocked: 0 right/wrong flips, 0 claims that capped on main and no longer cap. The fallback only moves claims from unread to read. - All the claims from my two earlier comments: the seven that must stay unread return
None, the fourmisleading/wrongones returnTrue. - A mutation run on the new code: 77 mutants, 51 killed. The survivors that matter are listed under the open test thread below.
- ReDoS: 28 adversarial inputs of about 1 MB (runs of separators, letters, spaces, commas) finish in 0.25 s or less.
Other points
- Nit: the evidence in the description predates the end-anchor. The 6 unread -> wrong and 7 unread -> right figures were measured on the fa1d016 parser. e5b0dff can only move entries back to unread (
misleading/wrong;,misleading/wrong (see note),reasonable, not wrong!are unread now). Please re-run the 6,424 entries and update the table.
Still open from my earlier review:
dokimi/tests/test_eval_judge.py:365(nit): most of the requested cases are in. Four mutants still pass the whole file: dropping.lower()on the negated branch (x: Not Rightthen reads as not wrong, 95 instead of 40), dropping.from the trailing[\s.)]*(x: misleading/wrong.goes unread), droppingre.Ifrom_VERDICT(64 B: WRONG (expected 128)goes unread, because the fallback now reads it), and dropping the dashes from the fallback (x — not wrong;x — misleading/wrongis read through the/, so the new em-dash case does not test the dash). Addx: Not Rightas 95/40,x: misleading/wrong.as 95/40,64 B: WRONG (expected 128)as 95/40 andx — not wrongto the is-read list.
Checked, no issues
- Old readings kept:
_VERDICTruns first, sox: wrong -> corrected: right,x: not wrong - wrongandx: wrong (see note)read as on main. - Multi-line claims: no
re.M, so$does not match at a line break. Addingre.MfailsTILE (default, wrong\non ARM): unverified, so the test pins it. - Repo rules: no schema change, no generated files, tests offline,
docs/methodology.mdstill matches the cap andunread_claims.
…erdicts Treat hedging adverbs (hardly, barely, scarcely, seemingly) like "not entirely" and keep claims unread when they precede a verdict. This prevents false caps on hedged statements like "x: dubious, hardly wrong" which should read as unread. Add test cases for edge cases where "/" or "," separate verdict-like words instead of marking qualified verdicts (e.g., "x: see /wrong", "x: unverified, right/wrong"). These now stay unread as intended. Reject such matches by checking if the word before the "/" or "," separator is itself a verdict word. Add explicit comments to _VERDICT_PART explaining it works as a gate, and update test case comments to describe the behavior being tested rather than referencing the mutation coverage process. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…add mutation test cases Use negative lookbehinds in _QUALIFIED_VERDICT regex instead of post-match logic to reject `/` when preceded by verdict words (right/wrong) or when not preceded by word chars (see /wrong). This eliminates the inline re import and 20-char window lookback. Update _VERDICT_PART comment to clarify it works as a gate. Add four mutation test cases to pin the regex parts they test: - `x: Not Right` (case-insensitive fallback) - `x: misleading/wrong.` (trailing punctuation) - `64 B: WRONG (expected 128)` (uppercase verdict) - `x — not wrong` (em-dash and negation) All four already behave correctly on HEAD. Update replies to mark point 1 as needs-owner, add reply for the Still open mutation coverage section, and clarify that `see docs/correct` still reads as right. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
|
On the review-body points:
|
…ther Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
antonwolfy
left a comment
There was a problem hiding this comment.
Review at 58208b1. The e5b0dff problems are mostly fixed: x: see /wrong, x: unverified, right/wrong and x: dubious, hardly wrong stay unread, and nothing main could read changes. The slash case is still open, though, and the hedge handling and its tests have gaps. Two of the inline comments I'd fix before merge.
What I ran, on a Linux review host at 58208b1:
make ci-local: 19 passed, 0 failed, 4 skipped. 4,354 tests passed and 1 skipped; ruff and mypy are clean; changed-line coverage is 100% (13 lines).- Old vs new
_is_wrong_figureover about 700,000 generated claims plus 250 hand-written ones (separators in the figure and in the verdict part, qualifiers,not, adverbs, hedges, dict claims, unicode dashes, multi-line, case), with the LLM transport mocked: 0 right/wrong flips, 0 claims that capped on main and no longer cap. Every change is unread -> read. - A mutation run on the new code: 79 mutants, 25 pass the whole of
test_eval_judge.py; 5 of those are equivalent. Details in the test comment below. - The PR's test file against main's
judge.py: 26 fail, so they pin the fix. - ReDoS: 29 adversarial inputs of 100 KB to 1 MB finish in 0.2 s or less.
Still open from my earlier review:
dokimi/dokimi/eval/judge.py:488: a/after any word character is still read as a verdict.x: see docs/wrongisTrue(capped to 40),x: unverified (see docs/wrong)and64 B: unverified, see section 4.2/wrongthe same, andx: see docs/correct,x: unverified (see docs/right)andx: unverified, matches the spec/correctread as right. All areNoneon main. Thesee /wrongfix does not cover a path whose directory is a word.dokimi/tests/test_eval_judge.py:278and:296:read as on main and still doandAdditional mutations: ...describe history, not behaviour.- The description is not updated: the diff block still shows
/and,added to_VERDICT; the evidence table was measured before the end-anchor;not rightandnot correctnow cap at 40 (8 ms: not right) and the description does not say so.
Checked, no issues
- Old readings kept:
_VERDICTruns first and is unchanged, so every claim main could read keeps its reading. - Hedges after
not:not entirely wrong,not hardly wrong,not necessarily right,not quite rightstay unread. - Lookbehinds on verdict words:
x: unverified, right/wrong,right, wrong,wrong, wrongstay unread;x: incorrect/wrongreads wrong through_VERDICT. - Search offset:
search(text)andsearch(text, part.start())give identical groups and spans on 148,000 generated and 400,000 random strings, so the comment that the offset changes nothing is correct. - Repo rules: no schema change, no generated files, tests offline.
Read other -ly adverbs as hedges (unread) instead of listing hedges, so 'entirely wrong' caps again; drop the redundant (?<!incorrect) lookbehinds and the no-op search offset; add the suggested mutation cases; rewrite the changelog fragment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RW8mAicb3ibXdzHMV83wDn Signed-off-by: Claude <noreply@anthropic.com>
…ict unread `x: <qualifier>, typically correct` hedges the verdict like `hardly`, so the claim stays in unread_claims; "typically" is deliberately not an intensifier. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RW8mAicb3ibXdzHMV83wDn Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
|
Re-ran our 91-string corpus at 2110cce for the single judge and the panel (both call The open slash case reproduces on this head: |
Only single-token qualifiers that start right after the separator or comma (misleading/wrong, L2/wrong) qualify a fallback verdict; see docs/wrong and see section 4.2/wrong stay unread. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RW8mAicb3ibXdzHMV83wDn Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RW8mAicb3ibXdzHMV83wDn Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RW8mAicb3ibXdzHMV83wDn Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
antonwolfy
left a comment
There was a problem hiding this comment.
Review at 45a61b8. The slash and hedge problems from my last review are fixed: x: see /wrong, x: see docs/wrong, x: unverified, right/wrong and x: dubious, hardly wrong stay unread, x: dubious, entirely wrong caps, and nothing main could read changes. One thing to fix before merge: the tests leave several branches of the new regex unpinned (inline, and the first item below).
What I ran, on a Linux review host at 45a61b8:
make ci-local: 19 passed, 0 failed, 4 skipped. 5,100 tests passed and 1 skipped; ruff, mypy, schema checks and the changed-line coverage check pass.- Old vs new
_is_wrong_figureover 400,000 generated claims (separators, dashes,/and,,not, adverbs, trailing punctuation, newlines, dict and non-string claims), with the LLM transport mocked: 0 right/wrong flips, 0 claims that capped on main and no longer cap. Every change is unread -> read. - ReDoS: 36 adversarial inputs of about 1 MB finish in 0.35 s or less, and the time scales linearly.
- A mutation run on the new code: 100 mutants, 30 of which pass the whole of
test_eval_judge.py; 5 of those are equivalent or artificial. - The PR's tests against main's
judge.py: 33 fail, so they pin the fix.
Still open from my earlier review:
dokimi/tests/test_eval_judge.py:322: the alternatives that read as right (x: unverified, wrong/right,x: unverified, wrong, right) are asserted as 95/95, and a claim read as right also scores 95, so they do not pin "unread". Dropping(?<!wrong)before the comma turnsx: unverified, wrong, rightfromNoneintoFalse, and all 149 tests still pass. Put them intest_a_figure_with_a_slash_or_a_hedge_is_not_read_as_right, which checksunread_claims == [0].- The description, as in my earlier nit and @xaleryb's comment: the diff block and the evidence table still describe an earlier parser. It also does not say that
not rightandnot correctnow cap at 40.
Checked, no issues
- Old readings kept:
_VERDICTruns first and is unchanged, so every claim main could read keeps its reading. - Hedges:
not entirely wrong,not necessarily right,x: dubious, hardly wrong,x: unverified, possibly wrongstay unread;entirely,completely,partiallyafter a comma cap. - Dict and non-string claims:
{"verdict": "misleading/wrong"}is wrong,{"verdict": "reasonable, not wrong"}is right, other types returnNone. - Repo rules: no schema change, no generated files, tests offline, changelog fragment present,
docs/methodology.mdstill matches the cap andunread_claims.
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
A claim read as right also scores 95, so the must-stay-unread cases did not pin "unread". Move them to the test that checks unread_claims == [0]. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RW8mAicb3ibXdzHMV83wDn Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…o the fallback Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
|
On the review-body points:
|
antonwolfy
left a comment
There was a problem hiding this comment.
Review at 1dee08b. Four of my five threads are fixed, the parser holds up, and nothing main could read changes. One thread stays open, and two more test gaps let real regressions through; I'd close them before merge.
What I ran, on a Linux review host at 1dee08b:
make ci-local: 19 passed, 0 failed, 4 skipped. 5,210 tests passed and 1 skipped; ruff, mypy, schema checks and the changed-line coverage check pass.- Old vs new
_is_wrong_figureover about 63,000 generated claims plus dict claims, with the LLM transport mocked: 0 right/wrong flips, 0 claims that capped on main and no longer cap. Every change is unread -> read (2,244 to right, 2,244 to wrong). - A mutation run on the new code: 105 mutants, 73 killed by
test_eval_judge.py. The survivors that matter are below and in the two test comments; the rest are equivalent. - ReDoS: inputs of up to 100,000 repetitions of separators, slashes, commas and
notfinish in under 0.2 s.
Still open from my earlier review:
test_eval_judge.py:312: the six cases are in and the(branch is pinned, but the dash andincorrectbranches of_VERDICTstill are not, and neither isright/correct. Every dash test ends at the verdict, so the fallback reads it when_VERDICTloses the branch. Mutants that pass all 156 tests: dropping the\s[-\u2013\u2014]alternative (64 B - wrong (expected 128)goesTrue->None), droppingincorrect(64 B: incorrect (expected 128)the same), droppingright(64 B: right (expected 64)goesFalse->None). Adding those three, plus64 B — incorrect, expected 128, as cap rows would close it.
Checked, no issues
- Old readings kept:
_VERDICTruns first and is unchanged, so every claim main could read keeps its reading. - Unread cases: the
unread_claims == [0]list now pins the alternatives, hedges, non-intensifier adverbs, prose and path tails;x: see docs/wrong,x: unverified, possibly wrong,x: unverified, right/wrongare all unread. - Docs:
docs/methodology.mdstill matches the cap andunread_claims; no schema change, no generated files, tests offline, changelog fragment present.
Per code review threads: - Add test cases to pin remaining mutation branches: case-insensitive intensifiers, multi-char qualifiers (C++, AVX-512), trailing punctuation, and negated incorrect. - Break the long comment line at 100 characters by wrapping after 'flips it.' - Clarify changelog wording: qualifier group repeats, so 'slash-separated tokens' not 'single token' qualify the verdict. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
2 open issues
What changed in this PR
This PR fixes verdict parsing in Judge._is_wrong_figure() so the “wrong figure caps correctness at 40” rule also applies when verdicts are qualified (e.g., misleading/wrong) or negated (e.g., not right).
Changes:
- Add a fallback regex-based parser to read end-anchored qualified/negated verdicts when the primary
_VERDICTpattern can’t. - Expand unit tests to cover qualified verdict lists, negations, separators, and false-positive cases.
- Add a changelog entry describing the new parsing behavior and preserved non-matches.
| File | Description |
|---|---|
| dokimi/dokimi/eval/judge.py | Adds fallback verdict parsing (_QUALIFIED_VERDICT) and supporting logic for qualified/negated verdicts. |
| dokimi/tests/test_eval_judge.py | Adds extensive test cases for qualified/negated verdict parsing and non-verdict false positives. |
| dokimi/changelog.d/20261016_122711_napetrov_verdict-parser.md | Documents the bugfix and the intended parsing boundaries. |
Review effort: Lite
| _QUALIFIED_VERDICT = re.compile( | ||
| r"(?:(?::|\s[-\u2013\u2014]|\(|(?<!wrong)(?<!right)(?<!correct),)\s*" | ||
| r"(?:(?!(?:wrong|right|correct|incorrect)/)[\w.+-]+/)*)" | ||
| r"(not\s+)?(?:([a-z]+ly)\s+)?(wrong|incorrect|right|correct)[\s.)]*$", | ||
| re.I, | ||
| ) |
There was a problem hiding this comment.
Not changed: named groups and re.VERBOSE would be a refactor beyond this PR; the cap table tests already exercise each group position.
Remove 'entirely' from _INTENSIFIERS frozenset because it hedges rather than intensifies a verdict. Update test cases that expected entirely to intensify a wrongness judgment to now expect no cap (unread). Fixes threads: - 4299242423: 'entirely' uncaps figures the judge called wrong - 4314442527: 'not incorrect' test needs assertion in cap table - 4299242446: Comment clarification on hedge list coverage Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
antonwolfy
left a comment
There was a problem hiding this comment.
Review at 0fb7df0. Three of the five threads are closed and the parser still reads everything main could read as before. Two things are worth fixing before merge: dropping entirely from the intensifiers brings back a missed cap, and the _VERDICT test gap from the last round is still open.
What I ran, on a Linux review host at 0fb7df0:
make ci-local: 19 passed, 0 failed, 4 skipped. 5,216 tests passed and 1 skipped; ruff, mypy, schema checks and the changed-line coverage check pass (15 lines, 100%).- Reproductions of
_is_wrong_figurewith the LLM transport mocked, on the claims below. - A mutation run on the new code: about 75 mutants, 17 of which pass the whole of
test_eval_judge.py. The ones that matter are below.
Other points
x: not completely wrongis unread only because of one untested condition._is_wrong_figurereturnsNonefor a negated intensifier (judge.py:481,if adverb and (negated or ...)). The only negated-adverb test usesnot entirely wrong, andentirelyis no longer an intensifier, so thenegated orpart is not exercised. Mutation check: replacing the condition withadverb and adverb.lower() not in cls._INTENSIFIERSpasses all 162 tests, andx: not completely wronggoesNone->False(read as right). Suggestion: addx: not completely wrongandx: not mostly wrongto theunread_claims == [0]list.
Still open from my earlier review:
dokimi/tests/test_eval_judge.py:322: the dash,incorrectandrightbranches of_VERDICTare still not pinned, and the 0fb7df0 diff adds none of the four cases. Mutants that pass all 162 tests: dropping the\s[-\u2013\u2014]alternative (64 B - wrong (expected 128)and64 B — incorrect, expected 128goTrue->None), droppingincorrect(64 B: incorrect (expected 128)the same), droppingright(64 B: right (expected 64)goesFalse->None). Add those four as cap rows (95/40, 95/40, 95/95 for therightone).dokimi/tests/test_eval_judge.py:326: the.lower()on the adverb is no longer pinned.x: dubious, Entirely wrongwas my case for it, and withentirelyout of_INTENSIFIERSit expects 95/95, so it passes with or without.lower(). Dropping.lower()onadverbin_is_wrong_figurepasses all 162 tests;x: dubious, MOSTLY wrongandx: dubious, Completely wronggoTrue->None. Use one of those as the 95/40 row.
Checked, no issues
- Old readings kept:
_VERDICTruns first and is unchanged, so every claim main could read keeps its reading. - Mutants killed: every separator alternative of
_QUALIFIED_VERDICT, the three lookbehinds, the$anchor, the.and)in the tail class, the path-qualifier lookahead words,re.Iand each of the five intensifiers. - Docs:
docs/methodology.mdstill matches the cap andunread_claims; the changelog fragment does not listentirelyand its examples match the code; no schema change, no generated files, tests offline.
The fallback comment had a 113-column line and a broken sentence; the gate comment named only the ASCII dash. Comment-only change. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
… first 'entirely' strengthens a verdict like 'completely' and 'totally', so 'entirely wrong' should cap a wrong figure. Restore it in _INTENSIFIERS and drop it from the hedge list. A negation is now resolved before the intensifier: 'not entirely correct' reads as wrong, while 'not entirely wrong' still hedges and stays unread. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
The fallback reads every plain dash, incorrect and right claim the old rows used, so dropping any of those branches from _VERDICT passed the suite while "64 B - wrong (expected 128)" stopped capping. Each new row has text after the verdict, which only _VERDICT reads. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
|
Re-checked at 385a1d6 on Linux: full suite, ruff, mypy and bandit, earlier review cases against main, and mutants of the
Nothing else is open. |
|
Checked at 16b1e70 (the fork head, your pin commit): |
antonwolfy
left a comment
There was a problem hiding this comment.
Review at 16b1e70. The last commits fix what I raised at 0fb7df0: entirely is an intensifier again, Entirely pins the .lower(), and the dash, incorrect and right branches of _VERDICT are pinned. The parser still reads everything main could read as before. What is left is the new negation rule and the tests around it, plus one comment line.
What I ran, on a Linux review host at 16b1e70:
make ci-local: 19 passed, 0 failed, 4 skipped. 5,224 tests passed and 1 skipped; ruff, mypy and the other checks pass.- Reproductions of
_is_wrong_figurewith the LLM transport mocked, on the claims in the inline comments. - A mutation run on the new code: 32 mutants, 30 killed by
test_eval_judge.py. The two survivors are in the inline comments. The three_VERDICTmutants from the last round (dash,incorrect,right) are now killed. - The PR's test file against the base
judge.py: 44 fail, so the tests pin the fix.
Checked, no issues
- Old readings kept:
_VERDICTruns first and is unchanged, so every claim main could read keeps its reading. - Intensifiers:
x: dubious, entirely wrong,x: dubious, Entirely wrongandx: dubious, MOSTLY wrongcap at 40;x: dubious, entirely correctdoes not. Dropping.lower()on the adverb or removing any of the six intensifiers fails the file. - Docs:
docs/methodology.mdstill matches the cap andunread_claims; no schema change, no generated files, tests offline, changelog fragment present.
| if adverb and negated and is_wrong_word: | ||
| return None # "not entirely wrong" hedges: partly right, or wrong | ||
| if negated: | ||
| return not is_wrong_word # "not correct" is wrong; "not entirely correct" too |
There was a problem hiding this comment.
not entirely correct and its siblings now cap correctness at 40, and not entirely wrong does not, so the two halves of the same hedge read opposite ways.
385a1d6 resolves negation before the intensifier. On this head Judge._is_wrong_figure returns:
x: not entirely correct->True,x: not partially correct->True,x: not mostly correct->True,x: not completely right->True(cap to 40).x: not entirely wrong->None,x: not completely wrong->None.
All of the first group are None on main. "Not entirely correct" is the usual way to say a figure is mostly right with a flaw, the same as "not entirely wrong" says the reverse. A judge that writes 8 ms: not entirely correct for a figure that is close now loses 55 points of correctness. The comment at judge.py:456 states the rule but not why correct should cap and wrong should not, and the changelog fragment does not mention it.
Suggestion: either treat a negated intensified verdict as a hedge in both directions (unread; drop (['x: dubious, not entirely correct'], 95, 40) and not partially correct from the cap table and move them to the unread_claims == [0] list), or keep the cap and say in the comment and the fragment that not <intensifier> correct is wrong on purpose, with the reason.
There was a problem hiding this comment.
Took the first option: a negated intensifier now hedges in both directions. not entirely correct, not partially correct, not mostly correct and not completely right stay unread, same as not entirely wrong. A bare not correct / not right still caps at 40. Pinned in the unread test and noted in the changelog fragment. Fixed in 7388adf.
| (["8 ms: dubious, entirely wrong"], 95, 40), | ||
| (["x: dubious, completely wrong"], 95, 40), | ||
| # A negated intensified "correct" is wrong: negation resolves before the intensifier. | ||
| (["x: dubious, not entirely correct"], 95, 40), |
There was a problem hiding this comment.
The negated-hedge guard is pinned for entirely only.
Mutation check: replacing if adverb and negated and is_wrong_word: (judge.py:485) with if adverb and adverb.lower() == "entirely" and negated and is_wrong_word: passes all 170 tests. Then x: not completely wrong goes None -> False (read as right). x: not mostly wrong, x: not partially wrong, x: not largely wrong and x: not totally wrong go the same way. Only 8 ms: not entirely wrong (:283, :393) is tested, so a later change that makes the hedge per-adverb passes, and a partly wrong figure reads as right.
Suggestion: add x: not completely wrong, x: not mostly wrong and x: not partially wrong to the unread_claims == [0] list (the one with 8 ms: not entirely wrong).
There was a problem hiding this comment.
Added not completely wrong, not mostly wrong, not partially wrong, not largely wrong and not totally wrong to the unread list, plus the correct-side negated intensifiers. Each one now fails under the per-adverb mutant. Fixed in 7388adf.
| "x — not wrong", | ||
| "x \u2013 not wrong", | ||
| # _VERDICT's `right` branch, with text after the verdict. | ||
| "64 B: right (expected 64)", |
There was a problem hiding this comment.
The correct branch of _VERDICT is still not pinned.
16b1e70 adds the right row here, but correct has none. Mutation check: dropping correct from _VERDICT (judge.py:438) passes all 170 tests, because the fallback still reads a bare x: correct. A verdict followed by text tells the two paths apart: x: correct, see note and x - correct (see note) return False on this head and None with the mutant, so they would land in unread_claims.
Suggestion: add x: correct, see note next to 64 B: right (expected 64) in the is-read list.
There was a problem hiding this comment.
Added x: correct, see note and x - correct (see note) to the is-read list, so dropping correct from _VERDICT now fails the suite. Fixed in 7388adf.
| r"(?::|\s[-\u2013\u2014]|\()\s*(?:[a-z]+ly\s+)?(wrong|incorrect|right|correct)\b", re.I | ||
| ) | ||
| #: Gate for the fallback: the claim must contain a ``:``, a dash after whitespace (``-``, | ||
| #: en or em dash) or ``(`` somewhere (it may be inside the figure). The fallback searches the whole claim; its end-anchor, not a start |
There was a problem hiding this comment.
Nit: this comment line is 138 characters (the project line length is 100; ruff does not select E501, so CI does not catch it). The re-wrap in 385a1d6 and b7aa61e left it merged. Wrap it after "the figure)." like the lines around it. _INTENSIFIERS at :467 is 102 characters and could go on three lines.
There was a problem hiding this comment.
Rewrapped the gate comment to 100 columns and split _INTENSIFIERS over three lines. Fixed in 7388adf.
`not entirely correct` capped at 40 while `not entirely wrong` stayed unread, so a close figure lost correctness on a hedge. Both now stay unread. Pin the `x: correct, see note` and `x - correct (see note)` branches and the other negated-intensifier hedges so mutants of `_VERDICT` and the hedge guard fail. Co-Authored-By: Claude Code <noreply@anthropic.com>


Follow-up to #55 (review: the shared verdict parser).
Summary
The wrong-figure cap reads each
numeric_claimsentry's verdict with one regex. A qualifier list ending in the verdict was invisible to it, so a figure the judge called wrong did not cap correctness.A verdict word with no separator in front stays unread: there the judge is saying the answer flagged a bad claim, not that its figure is wrong.
not rightandnot correctnow cap correctness at 40.Evidence
Old vs new
Judge._is_wrong_figure(head 308a23e, the real method; main's reading is_VERDICTalone) over 6,424 realnumeric_claimsentries from Haiku single-judge runs on the perf golden set. Re-parse only, no LLM call (rerun.py):<qualifier>/wrong(misleading/wrongx3,ambiguous/wrongx2,speculative/wrong)conditional/right,reasonable/right,reasonable, not wrong,(not wrong)x2,…, correct12 entries change in total.
typically correctis no longer in the list: it stays unread.test_a_wrong_figure_caps_correctness_at_forty[.../wrong]and the other qualified cases fail on main (correctness == 95, expected 40). After: pass. The unread cases (x: see docs/wrong,x: unverified, right/wrong,x: unverified, wrong, right,x: dubious, hardly wrong,correctly marked as WRONG to claim) are asserted throughunread_claims == [0], so a read as right (also 95) cannot hide a regression.wrong -> corrected: rightkeeps reading as wrong.pytest -q: 5205 passed, 2 skipped, 4 failed intest_model_defaults.py(litellm price lock, unrelated to this change);make ci-local: all other jobs pass (ruff, mypy, schema, changed-line coverage). Fresh CI run pending on the PR.Merge Danger
Door: two-way. One regex; revert restores the old reading.
Blast Radius: few scores
On the measured set, 6 of 6,424 entries now cap correctness to 40 where they did not. Affected judgements drop on the single judge, and on the panel once #55 lands (it shares the parser). 6 entries leave
unread_claimsas right. No prompt, schema or flag change. Judge prompts and the cache are untouched; the difference is applied when replies are read, so cached replies re-read with the new rule.Checklist
pytestpassesruff,mypy, and schema checks pass🤖 Generated with Claude Code