Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
77e3fab
frame: key audit entries by a record field, and declare side records
abrichr Oct 25, 2026
8406e0a
verify-run: exact_new_set sets aside a record another user added
abrichr Oct 25, 2026
c9f58f1
provenance: the round trip maps a document label to the stored code
abrichr Oct 25, 2026
9c6a6c4
benchmark: Phase 0 independent fault suite against the production rec…
abrichr Oct 25, 2026
15c6125
Merge remote-tracking branch 'origin/fix/frame-links' into measure/ph…
abrichr Oct 25, 2026
ec5dd74
Merge remote-tracking branch 'origin/fix/exact-new-set-attribution' i…
abrichr Oct 25, 2026
ff4d180
Merge remote-tracking branch 'origin/fix/roundtrip-value-map' into me…
abrichr Oct 25, 2026
bdb7586
qualify: record.surface reads the record from one of several surfaces
abrichr Oct 25, 2026
3d1eae2
Merge remote-tracking branch 'origin/fix/record-surface' into measure…
abrichr Oct 25, 2026
73161e7
benchmark: candidate OpenEMR contract with the Phase 0 fixes; phase0 …
abrichr Oct 25, 2026
7dab069
Merge remote-tracking branch 'origin/main' into measure/phase0-indepe…
abrichr Oct 25, 2026
666f0c0
benchmark: held-out fault suite (frozen, sha256 b1bd44ae) and its thr…
abrichr Oct 25, 2026
ea25134
benchmark: the held-out suite's described ops on the live fixture
abrichr Oct 25, 2026
55b4401
benchmark: held-out stand-in results (candidate 14/17, production 4/17)
abrichr Oct 25, 2026
96db2b2
frame: diff the whole database, with a reviewed allow-list
abrichr Oct 25, 2026
eec05d4
Merge remote-tracking branch 'origin/feat/frame-whole-database' into …
abrichr Oct 25, 2026
132d327
benchmark: whole-database candidate contract; phase0 --contract whole
abrichr Oct 25, 2026
3ad205f
frame: read rows wider than one JSON call, keeping null columns
abrichr Oct 25, 2026
e526c94
Merge remote-tracking branch 'origin/feat/frame-whole-database' into …
abrichr Oct 25, 2026
430ea8e
frame: a default the database computes at insert is a default
abrichr Oct 25, 2026
e0cbf2a
Merge remote-tracking branch 'origin/feat/frame-whole-database' into …
abrichr Oct 25, 2026
6e07428
frame: audit attribution can name a table's record through another field
abrichr Oct 25, 2026
ad09293
Merge remote-tracking branch 'origin/feat/frame-whole-database' into …
abrichr Oct 25, 2026
e0728d0
benchmark: the whole-database contract attributes contact rows by for…
abrichr Oct 25, 2026
b0b8f1b
benchmark: whole-database frame results on the stand-in (held-out sco…
abrichr Oct 25, 2026
c3987d9
benchmark: link the whole-database frame PR
abrichr Oct 25, 2026
5e72ecb
benchmark: fix the check commit for the second held-out suite before …
abrichr Oct 25, 2026
b90903a
benchmark: the second held-out suite (frozen, sha256 63b44803) and it…
abrichr Oct 25, 2026
96ae023
benchmark: second held-out suite on the stand-in (whole database 16/1…
abrichr Oct 25, 2026
93bd9ad
effects: a contract value can say how the store saves what was entered
abrichr Oct 28, 2026
c51a75d
Merge remote-tracking branches 'origin/feat/value-stored-form' and 'o…
abrichr Oct 28, 2026
5884b66
benchmark: the whole-database contract expects street and city in Ope…
abrichr Oct 28, 2026
6d0b152
benchmark: the stand-in saves street and city in capitals, as OpenEMR…
abrichr Oct 28, 2026
8898909
benchmark: fix the check commit for the third held-out suite; held-ou…
abrichr Oct 28, 2026
668d20d
benchmark: held-out suites move to private storage; public keeps hash…
abrichr Oct 28, 2026
5efb298
benchmark: harness shapes for the third held-out suite (no check chan…
abrichr Oct 28, 2026
9a87ecc
benchmark: held-out 3 aggregate (whole database 14/17, no fault passe…
abrichr Oct 28, 2026
bac815b
frame: whole-database diff without values, in one consistent read
abrichr Oct 28, 2026
90d6440
effects: stored forms change ASCII letters only; a binding comes from…
abrichr Oct 28, 2026
7c49019
effects: declared value kinds (phone, date, email)
abrichr Oct 28, 2026
02fb0d9
frame: excuse another actor's change to a declared record only throug…
abrichr Oct 28, 2026
f961c71
Merge remote-tracking branches 'origin/feat/frame-whole-database' and…
abrichr Oct 28, 2026
0381edb
benchmark: the whole-database contract with write attribution, allow-…
abrichr Oct 28, 2026
4eaf915
effects: exact_new_set matches declared kinds in canonical form
abrichr Oct 28, 2026
ae62bef
frame: a DEFAULT NULL column (shown as the text NULL) has no default
abrichr Oct 28, 2026
5d827f2
Merge remote-tracking branches 'origin/feat/frame-whole-database' and…
abrichr Oct 28, 2026
7f42842
frame: a side row of another actor's new record is theirs; report the…
abrichr Oct 28, 2026
a146172
verify-run: also set aside additions the frame names by surface id
abrichr Oct 28, 2026
85aa546
Merge remote-tracking branches 'origin/feat/frame-whole-database' and…
abrichr Oct 28, 2026
61b538d
effects: a duplicate key with a declared kind matches in canonical form
abrichr Oct 28, 2026
1821c79
Merge remote-tracking branch 'origin/feat/value-kinds' into measure/p…
abrichr Oct 28, 2026
7184c94
benchmark: after the review fixes, 17/17 on the stand-in on all four …
abrichr Oct 28, 2026
aa7d1ea
verify-run: the baseline holds the read set as keyed MACs, not values
abrichr Oct 28, 2026
0ba7fa7
benchmark: the docker-exec MariaDB shim keeps one session per connection
abrichr Oct 28, 2026
f475c35
benchmark: phase0 live grants the reader SELECT on the whole openemr …
abrichr Oct 28, 2026
e299dd6
benchmark: harness ops for the fourth held-out suite (no check changes)
abrichr Oct 28, 2026
88cedcf
benchmark: held-out 4 aggregate (whole database 17/17 on the stand-in…
abrichr Oct 28, 2026
49bc9ac
frame: the MySQL snapshot sends no statement that can fail
abrichr Oct 28, 2026
6b347c5
frame: two reads after the save must match
abrichr Oct 28, 2026
38ab956
Merge remote-tracking branch 'origin/feat/frame-whole-database' into …
abrichr Oct 28, 2026
0cf67b0
benchmark: first live MySQL run of the whole-database snapshot (witne…
abrichr Oct 28, 2026
f07a3d3
frame: rows by key in every table, attribution by kind and count, MAC…
abrichr Oct 28, 2026
e7455f4
Merge branch 'feat/frame-whole-database' into feat/sealed-baseline
abrichr Oct 28, 2026
ad96c7a
Merge #1582's round 2 into the sealed baseline; seal with the derived…
abrichr Oct 28, 2026
15f4533
Merge branch 'feat/frame-whole-database' into feat/frame-second-read
abrichr Oct 28, 2026
edb7435
frame: two reads after the save must match (on #1582's round-2 snapshot)
abrichr Oct 28, 2026
410c8e9
effects: stored forms use full Unicode case, as the browser does
abrichr Oct 28, 2026
9a825ee
Merge remote-tracking branch 'origin/feat/value-stored-form' into fea…
abrichr Oct 28, 2026
3c875d4
effects: value kinds through one normalizer; phone refuses other text…
abrichr Oct 28, 2026
51ca17e
app profiles: one versioned OpenEMR bookkeeping profile for the frame…
abrichr Oct 28, 2026
d2a18cb
Merge remote-tracking branch 'origin/feat/frame-second-read' into mea…
abrichr Oct 28, 2026
e273c8b
Merge remote-tracking branch 'origin/feat/app-profile' into measure/p…
abrichr Oct 28, 2026
c790a0c
Merge remote-tracking branch 'origin/feat/value-kinds' into measure/p…
abrichr Oct 28, 2026
3804609
benchmark: the whole-database contract uses the OpenEMR profile, dele…
abrichr Oct 28, 2026
a5477de
benchmark: the stand-in's allow-list filter tolerates a contract that…
abrichr Oct 28, 2026
48b9304
app profiles: contact is append-only bookkeeping (openemr-frame-profi…
abrichr Oct 28, 2026
cb82223
Merge remote-tracking branch 'origin/feat/app-profile' into measure/p…
abrichr Oct 28, 2026
856b312
benchmark: contact moves from a declared side record to the profile's…
abrichr Oct 28, 2026
e75b1d0
frame: tests use their own host secret; document that a deployment ke…
abrichr Oct 28, 2026
3040809
Merge branch 'feat/frame-whole-database' into feat/app-profile
abrichr Oct 28, 2026
d2d97e7
app profiles: compare credential tables; backfills only; an allowlist…
abrichr Oct 28, 2026
b421747
Merge remote-tracking branch 'origin/feat/frame-whole-database' into …
abrichr Oct 28, 2026
373ea40
Merge remote-tracking branch 'origin/feat/app-profile' into measure/p…
abrichr Oct 28, 2026
666f039
Merge remote-tracking branch 'origin/feat/sealed-baseline' into measu…
abrichr Oct 28, 2026
824491d
benchmark: the whole-database contract pins the OpenEMR profile's digest
abrichr Oct 28, 2026
cc06e4e
frame: the host secret is created whole and race-free on first use
abrichr Oct 28, 2026
899ae4d
Merge remote-tracking branch 'origin/feat/frame-whole-database' into …
abrichr Oct 28, 2026
6d2fc7d
Merge branch 'feat/frame-whole-database' into feat/app-profile
abrichr Oct 28, 2026
d130957
app profiles: a colleague who wrote in the window owns their per-user…
abrichr Oct 28, 2026
0c9287b
Merge remote-tracking branch 'origin/feat/app-profile' into measure/p…
abrichr Oct 28, 2026
616d037
benchmark: fix the check commit for held-out 5 (0c9287b85) before the…
abrichr Oct 28, 2026
de335e3
app profiles: keys and MFA registrations are compared in full (2026-1…
abrichr Oct 28, 2026
5843351
Merge remote-tracking branch 'origin/feat/app-profile' into measure/p…
abrichr Oct 28, 2026
784fc15
benchmark: pin openemr-frame-profile/2026-10-29.2
abrichr Oct 28, 2026
b774c45
benchmark: fix the check commit for held-out 5 at 784fc15e4, before o…
abrichr Oct 28, 2026
3cf03e4
benchmark: harness ops for the fifth held-out suite (no check changes)
abrichr Oct 28, 2026
5c634f4
benchmark: held-out 5 on the stand-in (whole database 17/17, counted)
abrichr Oct 28, 2026
40dfb07
benchmark: the fifth held-out suite's ops on the live fixture
abrichr Oct 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions packages/flow/benchmark/lifecycle_proof/readonly_db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"""A minimal DB-API connection to a fixture's MariaDB through ``docker exec``.

The OpenEMR fixture publishes no database port. This lets a ``kind: sql``
verifier (``sql_driver: benchmark.lifecycle_proof.readonly_db``) read it as a
SELECT-only user, the way a deployment would read through its own driver.

One connection is one ``mariadb`` client session, kept open for the
connection's life, so a transaction spans statements (the whole-database
snapshot reads everything inside one ``START TRANSACTION WITH CONSISTENT
SNAPSHOT``). After each statement a marker query ends its output. The client
runs with ``--force``, so a failed statement (an optional ``SET``) raises in
Python without ending the session.

Benchmark plumbing only: no query parameters, rows as text (``NULL`` is None).
"""

from __future__ import annotations

import os
import queue
import subprocess
import threading
import time
from typing import Any, Optional

_MARK = "__oa_end__"


class Error(Exception):
pass


class _Cursor:
def __init__(self, conn: "Connection") -> None:
self._conn = conn
self.description: Optional[list[tuple[str, ...]]] = None
self._rows: list[tuple[Any, ...]] = []

def execute(self, sql: str, params: Any = None) -> None:
if params:
raise ValueError("exec_mariadb takes no query parameters")
lines = self._conn._run(sql)
if not lines:
self.description, self._rows = [], []
return
header = lines[0].split("\t")
self.description = [(name,) for name in header]
self._rows = [
tuple(None if cell == "NULL" else cell for cell in line.split("\t"))
for line in lines[1:]
]

def fetchall(self) -> list[tuple[Any, ...]]:
return list(self._rows)


class Connection:
def __init__(self, container: str, user: str, password: str, database: str):
self._proc = subprocess.Popen(
[
"docker",
"exec",
"-i",
"-e",
"MYSQL_PWD",
container,
"mariadb",
"-u",
user,
"--batch",
"--raw",
"--unbuffered",
"--force",
database,
],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
env={**os.environ, "MYSQL_PWD": password},
)
self._errors: "queue.Queue[str]" = queue.Queue()
threading.Thread(target=self._drain, daemon=True).start()

def _drain(self) -> None:
assert self._proc.stderr is not None
for line in self._proc.stderr:
self._errors.put(line.strip())

def _run(self, sql: str, timeout_s: float = 300.0) -> list[str]:
assert self._proc.stdin is not None and self._proc.stdout is not None
statement = sql.strip().rstrip(";")
self._proc.stdin.write(f"{statement};\nSELECT '{_MARK}' AS {_MARK};\n")
self._proc.stdin.flush()
out: list[str] = []
done = threading.Event()

def read() -> None:
assert self._proc.stdout is not None
for line in self._proc.stdout:
line = line.rstrip("\n")
if line == _MARK and out and out[-1] == _MARK:
out.pop() # the marker's header line
break
out.append(line)
done.set()

threading.Thread(target=read, daemon=True).start()
if not done.wait(timeout_s):
self.close()
raise Error(f"no answer in {timeout_s:.0f} s")
time.sleep(0.02) # stderr is its own pipe: let a statement's error land
errors = []
while not self._errors.empty():
errors.append(self._errors.get())
if self._proc.poll() is not None:
raise Error("the mariadb session ended: " + " ".join(errors))
if errors:
raise Error(" ".join(errors))
return out

def cursor(self) -> _Cursor:
return _Cursor(self)

def close(self) -> None:
try:
if self._proc.stdin is not None and not self._proc.stdin.closed:
self._proc.stdin.close()
self._proc.wait(timeout=10)
except Exception: # noqa: BLE001 - closing best effort
self._proc.kill()


def connect(
container: str, user: str, password: str = "", database: str = "openemr"
) -> Connection:
return Connection(container, user, password, database)
118 changes: 118 additions & 0 deletions packages/flow/benchmark/lifecycle_proof/verify.database.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# CANDIDATE (not admitted): the OpenEMR referral contract with a whole-database
# frame. verify.yaml is the admitted one; verify.frame.yaml is the earlier
# candidate, whose frame covered only the tables it read.
#
# The frame diffs every table and column of the OpenEMR database before and
# after the save (frame.database). Any row added, removed or changed is
# collateral unless the record contract, the chart's history row
# (side_records) or a concurrent writer named by OpenEMR's audit trail
# explains it. contact (a link row per chart) is append-only bookkeeping
# in the OpenEMR profile. A new chart or history row that sets a column the contract
# doesn't name, outside new_row_fields, is collateral too.
#
# allow_tables is the reviewed list of tables OpenEMR writes on its own for any
# signed-in user: SPEC.md F3's bookkeeping list, and the tables the witness run
# found the New Patient path writing (WITNESS.md).
#
# Needs #1570, #1571, #1572, #1576 and the whole-database frame. Made-up
# patients only.
schema_version: openadapt.verify/v1
environment_id: openemr-local-fixture-7c96c8eefe46
minimum_effect_strength: independent_system_of_record

verifier:
kind: surfaces
surfaces:
patient:
kind: rest
base_url: ${OPENEMR_API_URL:-https://127.0.0.1:9300}
records_path: /apis/default/api/patient?_limit=1000
records_key: data
auth:
bearer_env: OPENEMR_ORACLE_TOKEN

record:
name: patient
surface: patient
match: [email, lname]
duplicate_key: email
exact_new_set: true
identity_field: uuid
fields:
email: {param: email, kind: email}
lname: lname
title: title
fname: fname
DOB: {param: DOB, kind: date, date_order: mdy}
sex: sex
# OpenEMR's New Patient form saves these in capitals (WITNESS.md, #1524).
street: {param: street, stored: upper}
city: {param: city, stored: upper}
state: {param: state_label, map: {Massachusetts: MA}}
postal_code: postal_code
phone_home: {param: phone_home, kind: phone}
country_code: country_code
# The fixture's two providers (users.id as seeded by the suite).
providerID: {param: provider, map: {"Dr. Iris Holt": "6", "Dr. Owen Hale": "7"}}

frame:
identity_field: id
side_records:
- {surface: history_data, link_field: pid, of_surface: patient, of_field: pid}
attribution:
kind: audit_read
actor_field: user
record_field: patient_id
record_key: pid
# Only a write to the same table attributes a change; a chart viewed is
# never a chart written.
event_field: event
write_events: [patient-record-insert, patient-record-update,
patient-record-delete, api-POST, api-PUT, api-PATCH, api-DELETE]
insert_events: [patient-record-insert, api-POST]
delete_events: [patient-record-delete, api-DELETE]
table_field: tbl
self: [openadapt_actor]
read:
kind: sql
sql_driver: benchmark.lifecycle_proof.readonly_db
sql_connect_args: &db
container: ${OPENEMR_DB_CONTAINER}
user: ${OPENEMR_READONLY_DB_USER:-oa_audit_reader}
database: openemr
sql_password_env: OPENEMR_READONLY_DB_PASSWORD
# A UI write's table is decoded from the statement OpenEMR logs in
# `comments`; an API write's from its endpoint.
sql_query: >-
SELECT CONCAT('log-', id) AS id, user, patient_id, event,
LOWER(REGEXP_REPLACE(FROM_BASE64(comments),
'(?is)^\\s*(?:insert\\s+into|update|delete\\s+from|replace\\s+into)\\s+`?([a-z_0-9]+).*$',
'\\1')) AS tbl FROM log
WHERE event LIKE 'patient-record-%' AND event <> 'patient-record-select'
AND patient_id > 0 AND user <> ''
UNION ALL
SELECT CONCAT('api-', a.id) AS id, u.username AS user, a.patient_id,
CONCAT('api-', a.method) AS event,
CASE WHEN a.request_url LIKE '%/allergy%' THEN 'lists'
WHEN a.request_url LIKE '%/message%' THEN 'pnotes'
WHEN a.request_url LIKE '%/insurance%' THEN 'insurance_data'
WHEN a.request_url LIKE '%/patient%' THEN 'patient_data' ELSE '' END AS tbl
FROM api_log a JOIN users u ON u.id = a.user_id
WHERE a.method IN ('POST', 'PUT', 'PATCH', 'DELETE') AND a.patient_id > 0
database:
dialect: mysql
read:
sql_driver: benchmark.lifecycle_proof.readonly_db
sql_connect_args: *db
sql_password_env: OPENEMR_READONLY_DB_PASSWORD
# OpenEMR's own bookkeeping, shared with rollback and the RL grader
# (openadapt_flow/app_profiles/openemr.yaml).
profile: openemr
profile_sha256: "sha256:a3daefcf7b7f96e0c68a7d5c1829c3e358dc1ce9366e3098d18bc80441685bed" # openemr-frame-profile/2026-10-29.2
views: {patient_data: patient}
new_row_fields:
# What OpenEMR's New Patient form fills on a new chart beyond the
# referral's fields (live witness, WITNESS_LIVE.md): ids, dates, and
# the form's empty strings where the column's default is NULL.
patient_data: [billing_note, birth_fname, birth_lname, birth_mname, care_team_status, created_by, date, dupscore, financial_review, gender_identity, guardianaddress, guardiancity, guardiancountry, guardianemail, guardianphone, guardianpostalcode, guardianrelationship, guardiansex, guardiansname, guardianstate, guardianworkphone, hipaa_allowemail, hipaa_allowsms, imm_reg_status, industry, interpreter_needed, nationality_country, occupation, patient_groups, pid, preferred_name, pronoun, protect_indicator, publicity_code, pubpid, ref_providerID, regdate, sex_identified, sexual_orientation, street_line_2, suffix, tribal_affiliations, updated_by, uuid]
history_data: [created_by, date, uuid]
92 changes: 92 additions & 0 deletions packages/flow/benchmark/lifecycle_proof/verify.frame.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# CANDIDATE (not admitted): the OpenEMR referral contract with the fixes the
# Phase 0 independent fault suite asked for. verify.yaml is the admitted one.
#
# Beyond verify.yaml it:
# - reads history_data, pnotes, lists and insurance_data (read-only SQL)
# beside the patient, so the frame sees writes there (record.surface);
# - requires one history_data row per new chart (frame.side_records);
# - checks the primary care provider, mapped from the name the referral
# prints to the users.id the chart stores;
# - attributes changes through OpenEMR's audit trail: UI writes in `log` by
# user name, API writes in `api_log` by user id, keyed by pid on every
# table (frame.attribution.record_key).
# The source check (provenance against the referral) is a run input
# (verify-run --provenance/--source), not part of this file.
#
# Needs #1570, #1571, #1572 and #1576. Made-up patients only.
schema_version: openadapt.verify/v1
environment_id: openemr-local-fixture-7c96c8eefe46
minimum_effect_strength: independent_system_of_record

verifier:
kind: surfaces
surfaces:
patient:
kind: rest
base_url: ${OPENEMR_API_URL:-https://127.0.0.1:9300}
records_path: /apis/default/api/patient?_limit=1000
records_key: data
auth:
bearer_env: OPENEMR_ORACLE_TOKEN
history: &readonly
kind: sql
sql_driver: benchmark.lifecycle_proof.readonly_db
sql_connect_args:
container: ${OPENEMR_DB_CONTAINER}
user: ${OPENEMR_READONLY_DB_USER:-oa_audit_reader}
database: openemr
sql_password_env: OPENEMR_READONLY_DB_PASSWORD
sql_query: SELECT * FROM history_data
notes:
<<: *readonly
sql_query: SELECT * FROM pnotes
allergies:
<<: *readonly
sql_query: SELECT * FROM lists
insurance:
<<: *readonly
sql_query: SELECT * FROM insurance_data

record:
name: patient
surface: patient
match: [email, lname]
duplicate_key: email
exact_new_set: true
identity_field: uuid
fields:
email: email
lname: lname
title: title
fname: fname
DOB: DOB
sex: sex
street: street
city: city
state: {param: state_label, map: {Massachusetts: MA}}
postal_code: postal_code
phone_home: phone_home
country_code: country_code
# The fixture's two providers (users.id as seeded by the suite).
providerID: {param: provider, map: {"Dr. Iris Holt": "6", "Dr. Owen Hale": "7"}}

frame:
identity_field: id
side_records:
- {surface: history, link_field: pid, of_surface: patient, of_field: pid}
attribution:
kind: audit_read
actor_field: user
record_field: patient_id
record_key: pid
self: [openadapt_actor]
read:
<<: *readonly
sql_query: >-
SELECT CONCAT('log-', id) AS id, user, patient_id FROM log
WHERE event LIKE 'patient-record-%' AND event <> 'patient-record-select'
AND patient_id > 0 AND user <> ''
UNION ALL
SELECT CONCAT('api-', a.id) AS id, u.username AS user, a.patient_id
FROM api_log a JOIN users u ON u.id = a.user_id
WHERE a.method IN ('POST', 'PUT', 'PATCH', 'DELETE') AND a.patient_id > 0
Empty file.
Loading
Loading