Skip to content

fault gate: admit a record check only after it catches its faults - #1445

Merged
abrichr merged 4 commits into
mainfrom
verification-coverage/fault-gate
Oct 20, 2026
Merged

abrichr merged 4 commits into
mainfrom
verification-coverage/fault-gate

Conversation

@abrichr

@abrichr abrichr commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Part 2 of #1434 (docs/VERIFICATION_COVERAGE.md). Stacked on #1443; the base is its branch.

Problem

A synthesized record check is built from observed values and available reads. scaffold-verifier tests it by mutating the records the check already read. That tests the judge, not the read. A check whose read is scoped to one patient can't see an edit to another patient's chart, however the judge is written, and the recorded-state test never notices.

Change

  • openadapt_flow/fault_gate.py: run_gate runs a check against a test copy of the system of record. Each case resets the copy, takes the check's baseline, performs the clean save and writes one bad save to the copy's store. The check then reads it through its own read, end to end through verify-run. The clean save must come out verified; each fault must not.
    • Fault classes: wrong_value (one per declared field), wrong_record (a look-alike), unrelated_edit (another record in the slice), partial, duplicate, missing, delayed (lands after the check's timeout).
    • check_digest: the effects, the read's shape and the frame. It leaves out where the store is and how to sign in, so a copy runs the same check as production, and a narrower read is a different check.
    • CoverageRecord (openadapt.check-coverage/v1, check_coverage.json) and AdmissionPolicy. The public default is a rule, not a tuned number: every applicable fault is caught, and coverage measured only on recorded states is refused. Deployment floors stay private.
    • Public test copies: MemoryRestCopy (an in-memory store over HTTP) and SqliteCopy. Fixture copies for OpenEMR and openIMIS come with the benchmark PR.
  • verify-run admission:: under a policy the signed receipt carries checked.coverage = {killed, applicable, sha256, policy}, and verified requires policy: met. A missing, stale or other-check record blocks it.
  • scaffold-verifier --fault-gate runs the gate on a copy of --db, and a surviving fault keeps the check a draft. Without the flag, the recorded-state test is written in the same format and is incomplete on purpose: it can't test a wrong value written over the right record, or a late write.
  • Frame fix, on verify-run: frame conditions (nothing outside the declared delta changed) #1443: a content twin (records_drop_fields: [id]) is a view of the same read, so records declared in one are declared in both. Without it, the frame flagged a scaffold-verifier check's own saved row.

What the gate found, and the fix

--db synthesis scopes the target table's read by the patient (WHERE patient_id = :p0). On a SQLite copy, an edit to another patient's row in the same table survived the gate; the recorded-state test had passed it (test_the_gate_finds_a_read_scoped_to_the_patient). Synthesis now also reads the target table whole, so the frame sees every undeclared row, and the gated check kills every fault (test_scaffold_verifier_on_the_gate_reads_the_target_table_whole). The frame-condition review fixes from #1443 are merged in.

Safety review

This adds refusals only: a policy can block verified, and nothing turns a refusal into a confirmation. Without admission: nothing changes. Still, it touches when verified is issued, so it waits for the coordinator's review with #1443.

Tests

  • tests/test_fault_gate.py (8): a full read kills all 9 applicable faults; a patient-scoped SQLite read lets unrelated_edit survive; the digest ignores the store location and sees a narrower read; admission problems; verify-run signs the coverage, and a missing or stale record blocks verified; scaffold on the gate.
  • 231 passed locally across the gate, frame, verify-run, guard, record-check synthesis, scaffold-verifier, countersign and declarative qualify suites. ruff; mypy clean on the changed modules.

🤖 Generated with Claude Code

A check built from the same place as the save proves little until it has
been seen to fail. The gate runs the check against a test copy of the
system of record, writes seven kinds of bad save to the copy's store
(wrong value per field, look-alike record, unrelated edit, partial,
duplicate, missing, delayed) and lets the check read them through its own
read, end to end through verify-run. A read whose scope can't see a fault
fails the gate.

- openadapt_flow/fault_gate.py: run_gate, check_digest (effects, read
  shape and frame; not the store's location), CoverageRecord
  (openadapt.check-coverage/v1), AdmissionPolicy, and two public test
  copies (MemoryRestCopy, SqliteCopy).
- verify-run: admission: section; checked.coverage in the signed receipt
  ({killed, applicable, sha256, policy}); verified requires policy met.
- scaffold-verifier --fault-gate: the gate on a copy of --db. Without it,
  the recorded-state test is written in the same format, incomplete on
  purpose (it can't test a wrong value over the right record or a late
  write).
- countersign shows the coverage; qualify ignores admission:.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
abrichr and others added 2 commits October 20, 2026 14:10
The fault gate showed that a --db check whose read is scoped to the
patient can't see an edit to another patient's row in the same table.
Synthesis now adds an unscoped read of the target table, so the frame
sees every row the save didn't declare, and the gated check kills every
fault in the suite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from verification-coverage/frame to main October 20, 2026 20:24
@abrichr
abrichr added this pull request to the merge queue Oct 20, 2026
Merged via the queue into main with commit db232a9 Oct 20, 2026
39 checks passed
@abrichr
abrichr deleted the verification-coverage/fault-gate branch October 20, 2026 23:29
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