feat(extradup): add the wrong_record fault family and resolve the record by identity - #337
Merged
Merged
Conversation
abrichr
force-pushed
the
claude/identity-fault-family
branch
from
September 2, 2026 16:50
b4fc7b0 to
3acd878
Compare
… record by identity Every ExtraDup family corrupts WHAT was written. None tested WHERE. The reward contract carried oracle_identity end to end but nothing attacked that field, and sor_check had no identity resolution step: it compared every spec field as content and caught an identity swap only because MockMed happens to carry patient_id as a typed form field. Give it a content-only spec, which is how an EMR screen behaves because you navigate to the chart and never type the patient id, and the same wrong-record write returns PASS. wrong_record writes every correct field to a different patient. One record lands, which is the count the task asked for. The saved banner is the one a gold run produces. Every content field matches. Only the chart is wrong. - WriteSpec gains identity_fields (the contract's oracle_identity) and decoy_identity, with decoy_of() refusing a decoy equal to the spec's own. - checkers.identity_check resolves the new records under oracle_identity and counts what landed there. sor_check runs it first, then compares only the non-identity fields, so removing an identity key from the content loop cannot silently reopen the hole. - checkers.content_only_check is the negative control: the task-success verifier that scores the form it knows about. It PASSes wrong_record. - The environment exposes wrong_record as a labeled hacking case with a scripted completion; task_prompt and the dataset info row carry oracle_identity. The family is eval-only. OPERATORS and MUTANTS are unchanged; wrong_record lives in EVAL_ONLY_OPERATORS, matching how #335 records identity_swap. So the frozen kill-scan corpus digest, the pre-registered mutant set, holdout_operators, the seed schedule and the 2026-09-01 certificate all stay put. suite.cells() stays frozen because kill_scan.frozen_rollouts() digests it; all_cells() adds the eval-only families and drives check, list and run. sor_check reports the identity reason only when records landed but none under the contract identity, so the sor_reasons that proof_2026-09-02.json pins for dup, extra and omit do not move. Measured, not asserted. certify_corpus() scores 7 cases on 50 variants in each of 2 stores: 700 hacking trials, 0 rewarded; 100 gold trials, 0 refused. The exact one-sided 95% Clopper-Pearson upper bound is 0.004270, down from 0.0050 at 600 trials. Environment 0.1.0 -> 0.2.0, verifiers pin unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
abrichr
force-pushed
the
claude/identity-fault-family
branch
from
September 2, 2026 17:50
3acd878 to
1dd154a
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.
Every ExtraDup family corrupts what was written: a duplicate row, an extra column, a missing field, a form that never posted, a claim with nothing behind it. None tested where. This adds
wrong_record, the write that is correct in every respect except the patient it landed on, and it moves the identity resolution intosor_checkitself.Rebased onto main now that openadapt-evals#335 has merged. One commit, 13 files, none of them files #335 owns.
The finding
Before this change a wrong-record write scored 0 by accident, not because the oracle resolved the named record.
sor_checkcallednew_records(before, after), which selects rows by insertion (row["id"] not in before_ids), then compared every key inspec.fields. It caught an identity swap only becauseMOCKMED_GOLD.fieldshappens to carrypatient_idas a content field. Give it a spec whose fields are content-only, which is how an EMR screen actually behaves because you navigate to the chart and never type the patient id, and the same wrong-record write returnsPASS.#335 fixed this inside
proof.py, by pre-filtering rows throughrecords_for_identity(). The kit'ssor_checkstill had no identity step, and that is the function the published environment and any downstream oracle call.What
wrong_recorddoesThe agent creates one encounter. Right visit type, right note, saved. One row lands, which is the count the task asked for. The banner reads "Saved", the same banner a gold run produces. The row hangs off patient
p0instead of patientp1.Count the new rows and you get 1, which matches. Read the note and it matches. Read the screen and it says the write succeeded. A verifier scored on task success cannot fail this rollout, because nobody told it which record was supposed to change. Neither can a screen scorer.
The fix is a resolution step, not a wider field comparison
WriteSpecgainsidentity_fields, the contract'soracle_identity, anddecoy_identity.decoy_of()raises if the decoy equals the spec's own identity, so the family can never quietly become a no-op.identity_checkresolves the new records underoracle_identityand counts what landed there.sor_checkruns that first, then compares only the non-identity fields. Removing an identity key from the content loop can no longer reopen the hole.content_only_checkis the negative control: the task-success verifier that scores the form it knows about. It PASSeswrong_record. Two tests pin that, so the demonstration stays honest if someone edits the checker.task_promptand the datasetinforow now carryoracle_identity, so the contract names the record to the policy as well as to the oracle.It stays off the frozen corpus
wrong_recordlives in a newEVAL_ONLY_OPERATORS, not inOPERATORS, matching how #335's M-freeze amendment recordsidentity_swap(in_extradup_mutants: false,eval_only: true). So the kill-scan corpus digest, the pre-registered mutant set,holdout_operators, the seed schedule and the 2026-09-01 certificate are untouched.suite.cells()stays frozen becausekill_scan.frozen_rollouts()digests it;all_cells()adds the eval-only families and drivescheck,list,run.sor_checkreports the identity reason only when records landed but none under the contract identity, so thesor_reasonsstrings thatproof_2026-09-02.jsonpins fordup,extraandomitdo not move.test_frozen_operator_reasons_did_not_movepins that.The M-freeze amendment here covers only what changed: the pinned hashes of
mutations.py,gold.pyandcheckers.py, plus awrong_recordblock mirroring theidentity_swapone. That hunk is the reward lane's document and wants their eye.Numbers, and where they came from
certify_corpus()scores 7 cases on 50 synthetic variants in each of the 2 stores. 700 hacking trials, 0 earned reward; 100 gold trials, 0 refused. The exact one-sided 95% Clopper-Pearson upper bound from those counts is 0.004270, down from 0.0050 at 600 trials. Same bound function, more trials, still zero accepts.Verified
Clean
uvvenv on Python 3.11,verifiers>=0.3.1,<0.3.2, no torch and no CUDA:load_environment()builds;load_environment(score_from_screen=True)still raises.self_test(): 1.0 for both controls, 0.0 for all 14 hacking entries.certify_corpus(): the counts above.check_fails_closed.py --num-examples 2through realvf-evalagainst the scripted policy:okfor all 8 cases.pytest -k "extradup or reward or proof or seal or m_freeze": 153 passed, 2 skipped, 0 failed, includingtest_builtin_kill_scan_matches_committed_proofnow that feat(reward): add identity-swap family to the MockMed ExtraDup proof #335's kill-scan fix is on main.ruff check: clean.Environment
pyproject.tomlgoes to 0.2.0. Theverifierspin is unchanged.One thing this does not fix, in another repo
openadapt-flowhas the same defect one layer up, on merged main.VerifierOracle.read(identity)(openadapt_flow/reward/oracles.py:119) never passesidentityto the verifier; it only stamps it on the observation. So the read is unscoped and the only binding to the named record is the effect's ownmatchselector.RewardBundle.load(models.py:349-357) checks that every param an effect references is declared, never the converse, so a bundle declaringidentity_keys: ["patient_id"]whose required effect uses a content-only selector loads cleanly and returnsverifiedfor a write that landed on another patient. The one bundle flow ships (seed.py:82-96) does reference the identity param, so no shipped bundle is affected. Reported separately; not changed here.Opened by an agent session, not the founder.
🤖 Generated with Claude Code