Skip to content

benchmark: Phase 0 held-out and independent fault suites against the OpenEMR record check - #1580

Draft
abrichr wants to merge 98 commits into
mainfrom
measure/phase0-independent
Draft

abrichr wants to merge 98 commits into
mainfrom
measure/phase0-independent

Conversation

@abrichr

@abrichr abrichr commented Oct 25, 2026 •

Copy link
Copy Markdown
Member

Phase 0 measurement of the OpenEMR referral record check against an independent fault suite and a held-out suite. Draft: the live OpenEMR run is queued for the fixture lease.

Held-out suite (heldout.yaml, authored blind, sha256 b1bd44ae…, frozen before any run, no case edited):

contract stand-in, commit 666f0c0
production (lifecycle_proof/verify.yaml) 4/17
candidate (lifecycle_proof/verify.frame.yaml with #1570, #1571, #1572, #1576) 14/17

The gate (at least 15/17, no fault passed as verified) is not met. The candidate passes three bot writes outside the tables it reads.

Original suite (faults.yaml, same commit): production 7/17, candidate 17/17. The candidate's fixes were written knowing these cases, so the held-out score is the one that counts.

The harness adds phase0 --suite. It implements the three operations the held-out author described in words, plus inline save values and a full person map. Nothing under openadapt_flow/ changes beyond the merged fix branches.

Whole-database frame (#1582, --contract whole, commit e0728d0): 17/17 on the original suite and 17/17 on this held-out set. The held-out 17/17 does not count: this set's misses shaped the design, and two false alarms found while running it were fixed first. A second blind held-out set will be the measurement. The read-list candidate's 14/17 stays the score for this set.

Second held-out suite (heldout2.yaml, authored blind after the whole-database frame, sha256 63b44803…). The check commit c3987d9 was recorded and pushed before the file was opened. Results are on the stand-in, at commit b90903a (harness changes only):

contract right fault passed as verified
whole database (#1582) 16/17 none: gate met
read-list candidate 15/17 1
production 6/17 9

The one wrong verdict for the whole-database contract is a clean case flagged as a fault. The record check compares street and city exactly, and OpenEMR stores them in capitals (#1524). Live OpenEMR is still queued.

Source policy: the held-out suites, their per-case results and briefs are private (OpenAdaptAI/openadapt-internal#76). This PR keeps each suite's hash, its counts per class and the aggregate scores (HELDOUT.md). faults.yaml stays as the published sample.

🤖 Generated with Claude Code

abrichr and others added 25 commits October 25, 2026 10:19
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ord check (harness)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With verifier.kind: surfaces, the record shorthand can name the surface its
record comes from, so a contract that qualify and verify-run share can also
read other tables for its frame and side records. Unset, effects (and so
contract digests) are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…plus reads it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ee described ops

phase0 --suite runs heldout.yaml. The stand-in implements the ops the
held-out author described in words (an appointment, a doubled history row,
a revert of another user's edit), inline register values and a full
person map, without editing a case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frame.database snapshots every table and column before and after the save
(three statements, whatever the table count) and judges each table as a
slice. A row added, removed or changed is collateral unless a declared
effect, a side record or an attributed concurrent writer explains it. A new
declared or side row that sets a column nothing declares is collateral too.
Tables over max_rows are fingerprinted. allow_tables, reviewed per app,
replaces the read-list; an unreadable snapshot leaves the frame not checked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SQLite caps a function at 127 arguments and OpenEMR's chart has more
columns, so the snapshot failed and the frame was not checked. Each row is
now read as JSON objects of at most 50 columns, merged in Python (a JSON
merge patch would drop null values).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eign_id

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…re not counted)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
abrichr and others added 4 commits October 25, 2026 11:22
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reading it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s four described ops

Harness only: a deleted insurance row (with the row it deletes placed before
the window), an empty second history row, a save stored in capitals, a
revert of another user's edit, and a person's provider. No check, contract
or case changes since c3987d9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…7, no fault passed)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
abrichr and others added 30 commits October 28, 2026 16:42
…; the document's own date order

From the second review of #1597:
- kind: phone reads a value only if it holds nothing but digits, spaces,
  ( ) + - . / and one trailing extension; '617-555-0142 do not call' is not
  a phone number, so it never matches one.
- A date may be followed by a time of day only.
- resolve() returns the entered value; kinds apply only where values are
  compared (ValueExpr.matches), so an on-screen read-back or an undo request
  still sees what was typed.
- One normalizer: readback_values gains phone and email, and the record
  check, the round trip and the screen read-back all use it.
- The round trip reads the source document in its own date order
  (ParamProvenance.date_order), never the app's; without one, a date it
  prints two ways stays unchecked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, rollback and RL

openadapt_flow.app_profiles.load_profile('openemr'): the tables and columns
OpenEMR writes on its own (logs and id maps append-only; per-session state
skipped with a reason; uuid backfill and the users ON UPDATE stamp as column
rules), from the RL environment's calibration (2026-10-28.1) and the frame's
live witness. No rule may name a patient-data table except for a system
column. frame.database.profile applies it; the receipt names the version,
its digest and the rules that excused something in the run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hase0-independent

# Conflicts:
#	packages/flow/openadapt_flow/runtime/effects/frame.py
…te events and the witness's new-row columns

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… names only a profile

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…le/2026-10-28.2)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… append-only bookkeeping

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eps it from the agent

The test suite created the real ~/.config/openadapt/frame.key; an autouse
fixture now sets OPENADAPT_FRAME_SECRET per test. The default key file only
keeps the secret out of the baseline, since a same-user agent can read it:
a deployment sets the secret from a store the agent can't read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…; a pinned digest

From the third review of #1604:
- No table is skipped any more. Credentials and access are compared:
  users_secure and login_mfa_registrations excuse only sign-in counters and
  timestamps on the acting user's own row; keys are append-only;
  background_services excuses only next_run and running; user_settings and
  recent_patients only the acting user's own rows (owner by user id, looked
  up in the snapshot).
- uuid rules excuse a backfill only, from NULL to a value; users.last_updated
  only beside one.
- The patient-data denylist is now an allowlist: a rule may name only a
  reviewed bookkeeping table (lower-cased, unqualified); '*' only a system
  column backfill.
- Profiles load only from the package, by name; frame.database.profile_sha256
  pins the digest (bound into frame_sha256), and a mismatch is refused.
- rules_used lists only the rules that excused something in the run.
- A table without a primary key keys its rows without the columns a rule
  lets move. The profile is registered in public-artifacts.json.

Profile version openemr-frame-profile/2026-10-29.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hase0-independent

# Conflicts:
#	packages/flow/openadapt_flow/runtime/effects/frame.py
#	packages/flow/openadapt_flow/verify_run.py
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two processes starting together could read a half-written key file and
MAC the baseline and the read after it with different keys. The secret is
now written to a private temp file and linked into place; the loser reads
the winner's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… UI state

own_rows rules (user_settings, recent_patients) also excuse the rows of
another user with a write entry in the window: their own work. Credential
column rules stay on the acting user's row. The users table's ids and names
are read in the snapshot, in memory only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uite exists

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…0-29.2)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repointing a row to another chart (insurance, contact, history), a
colleague's setting flipped, a colleague's password hash reset, a field
reverted, providers named as 'users.id of'; pre-window rows for the
insurance and the colleague's setting; the stand-in keeps users_secure.

This branch has not been deployed

No deployments
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