fix(benchmark): check the MockMed encounter type as its own field - #302
Merged
Conversation
abrichr
force-pushed
the
fix/mockmed-verify-encounter-type
branch
from
July 28, 2026 08:17
39f491e to
513aeb6
Compare
Member
Author
|
Launch-train status (2026-07-28): this PR remains intentionally held behind the exact fresh-frame/OpenEMR campaign gate. Development continues in parallel; this is only a merge-order constraint on Flow main. Before merge, the corrected benchmark verdicts need the independent oracle and artifact-manifest review recorded in the release plan, followed by one fresh-main rebase and one authoritative exact-head matrix. No repeated CI reruns are needed meanwhile. |
`verify_encounter_saved` is the arm-independent success criterion for the
compiled-vs-agent MockMed benchmark. It matched the saved encounter row as
one fuzzy candidate string, `<type> — <note[:60]>`, at min_ratio 0.8. A
single similarity ratio over that concatenation is decided by whichever
field contributes more characters, so a long correct note outvotes a short
wrong type.
Measured on a retained MockMed frame (E4 precondition run, episode 4 of
20): the agent saved the correct note under encounter type `Consult`
instead of the requested `Triage`, and the check passed it.
expected : 'Triage — Follow-up in 2 weeks; BP recheck.'
actual : 'Consult — Follow-up in 2 weeks; BP recheck.'
ratio : 0.8471 >= 0.8 -> MATCHED
A second, independent path made the type unverifiable at all: the row's
fallback candidate was the bare `note_text[:40]`, which the banner's own
note line satisfies. On that same frame it scored 0.918 while the banner
evidence the check *needs* scored only 0.875, so no threshold separates
the two: any min_ratio that rejects the wrong-type row also rejects every
genuine success. Measured over 61 retained trajectories, sweeping
min_ratio leaves both false positives standing up to 0.86 and collapses
all 27 true positives to false negatives at 0.88. Raising the threshold
is not a fix.
The fix is structural. Each field is judged on its own terms: the row line
is parsed into its type field and its note field, the type field is
CATEGORICAL and must name exactly one member of the application's
enumeration (`ENCOUNTER_TYPES`) and that member must be the requested one,
and the note field keeps the unchanged min_ratio fuzzy tolerance. A row
carrying this run's note under a different known type now sets a new
`wrong_type_row` field and fails the run, matching what
`verify_hybrid_final` already reports for the hybrid arms.
Validation over all 61 retained E4 trajectories (40 carry an independent
in-page record-oracle label; 21 are labelled from the rendered encounter
list), both directions:
before TP=27 FP=2 TN=32 FN=0
after TP=27 FP=0 TN=34 FN=0
Only the two known-bad episodes change verdict; every known-good episode
still passes. On the 27 true positives the type field scores 1.0000
against a 0.8 floor and the note field 0.9000, so the fix is not near a
false negative.
Free-text tolerance is unchanged: every true positive already satisfied
the `<type> — <note>` row candidate at 0.8974, so removing the type-free
fallback costs no genuine success on this corpus.
Corrected figures for the E4 precondition run, denominator 20 episodes per
model per oracle: 7B screen oracle 14/20 -> 13/20, which now agrees with
the in-page record oracle's 13/20; 3B unchanged at 0/20 on both.
Tests exercise both directions at four note lengths (11/22/33/60 chars),
covering the band where the old ratio crossed the threshold (0.7317,
0.8254, 0.8706, 0.9209 against the expected Triage row form).
abrichr
force-pushed
the
fix/mockmed-verify-encounter-type
branch
from
July 28, 2026 15:56
513aeb6 to
2d9c736
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The defect
verify_encounter_savedis the arm-independent success criterion for thecompiled-vs-agent MockMed benchmark. Both arms are judged by it, so the
comparison between arms is unbiased — but every absolute rate it has
produced is too high.
It matched the saved encounter row as one fuzzy candidate string,
<type> — <note[:60]>, atmin_ratio = 0.8. One similarity ratio over thatconcatenation is decided by whichever field contributes more characters, so a
long correct note outvotes a short wrong type.
Measured on a retained MockMed frame (E4 precondition run, episode 4 of 20):
the agent saved the correct note under encounter type
Consultinstead of therequested
Triage, and the check passed it.Verifying the defect against the actual screenshot found a second,
independent path that the original write-up did not name, and it is the
dominant one. The row check's fallback candidate was the bare
note_text[:BANNER_NOTE_CHARS]— a candidate with no type in it at all.rapidocr segments MockMed's banner into
Encounter saved -plus its own noteline, and that note line satisfies the fallback outright. On the episode-4
frame:
Encounter saved —(banner)'Encountersaved-'Triage — <note>(row)'Follow-upin2weeks;BPrecheck.'<note>(row fallback)'Follow-upin2weeks;BPrecheck.''·Consult—Follow-up in2weeks;BPrecheck.'(the real row)Why not raise the threshold
There is no threshold that works. On that frame the false-positive
evidence (0.918) is stronger than the true evidence the check requires
(0.875 for the banner). Swept over all 61 retained trajectories:
min_ratioBoth false positives survive to 0.86; at 0.88 every true positive collapses
into a false negative. The threshold has no separating value.
Removing only the type-free fallback is also not sufficient: the row
candidate still scores 0.8000 on that frame, exactly at the threshold, off a
line that contains no type.
The fix
Structural, not a threshold nudge — each field is judged on its own terms.
(split at the first dash-like character, plus a positional split per known
type so a dropped separator still parses).
application's enumeration
ENCOUNTER_TYPES = ("Triage", "Consult"), andthat member must be the requested type. A wrong
Consultcan no longer beabsorbed by a right note body, at any note length.
min_ratiofuzzy tolerance. Nothing about free-text matching is tightened.
wrong_type_rowfield and fails the run — reporting silent wrong-action, asthe reliability standard in
AGENTS.md§3 requires.This converges
verify_encounter_savedon the disciplineverify_hybrid_finalalready uses in this same package: that functionscores the type and the note as separate line-level evidence and reports
wrong_type_row, and its docstring already names this exact failure mode.The MockMed benchmark's criterion was simply never brought along.
Validation — both directions, all 61 trajectories
The regression set is the 61 retained E4 trajectories with screenshots: 40
carry an independent in-page record-oracle label (
window.state.encountersread via
page.evaluate— bypasses rendering, OCR and fuzzy matching), and 21are labelled from the rendered encounter list.
origin/main)Exactly two episodes change verdict, and both are the known-bad one (the
same seed in both 7B passes). Every known-good episode still passes. There is
no false negative in either direction.
Margins on the 27 true positives, against a 0.8 floor:
And the free-text tolerance is provably unchanged: every one of the 27 true
positives already satisfied the
<type> — <note>row candidate at 0.8974, sodropping the type-free fallback costs no genuine success on this corpus.
Corrected numbers, with denominators
Denominator n = 20 episodes per model per oracle, pre-registered before any
episode ran and never adjusted.
Episode 4 moved in both 7B passes. The corrected screen oracle now agrees
with the independent record oracle at 13/20 in both, which strengthens rather
than weakens the run's replication claim.
Tests
Both directions at four note lengths (11 / 22 / 33 / 60 characters), spanning
the band where the old ratio crosses the threshold — 0.7317, 0.8254, 0.8706,
0.9209 against the expected
Triagerow form. The benchmark's own 32-characternote sits inside the missed band.
test_right_type_passes_at_every_note_length— the over-halt guard. Acheck strict enough to trip on OCR noise would make a working system look
broken, which is worse than the over-count it replaces.
test_wrong_type_fails_at_every_note_length— the regression.test_wrong_type_row_beside_right_row_failstest_bare_note_line_is_not_an_encounter_row— the second defect path.test_unlisted_requested_type_is_still_checkedMutation-checked: with the fix reverted (old row logic restored, result
fields kept so the failures are behavioural rather than
AttributeError),8 tests fail, including all four wrong-type note lengths. Restored, all pass.
Scope
verify_encounter_savedhas exactly one production caller,benchmark/run_benchmark.py(the MockMed compiled-vs-agent benchmark).The OpenEMR benchmark uses
verify_note_saved, a different criterion with nocategorical field, and is not affected by this defect; it is unchanged
here.
verify_hybrid_finalwas already correct and is unchanged.VerifyResultgainswrong_type_rowwith a default, so existing rows inbenchmark/results.jsonstill parse.normalize_textis now exported fromopenadapt_flow.vision(it was alreadypublic in
vision.ocr).🤖 Generated with Claude Code
https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM