Repository navigation
Learned excuses: learn the app's own per-request writes, then enforce exactly what a person approved - #1679
Open
abrichr wants to merge 12 commits into
Open
Learned excuses: learn the app's own per-request writes, then enforce exactly what a person approved#1679abrichr wants to merge 12 commits into
abrichr wants to merge 12 commits into
Conversation
…ures, refused writes are UNCERTAIN, declared only unchecked episodes, FHIR tests)
… check; sql: verifier.recheck), late REFUTED in its own report section; CTO summary by kind
… timing, CAPABILITIES and CHANGELOG
# Conflicts: # packages/flow/CHANGELOG.md
… exactly the approved moves
…d; docs, CLI test
…until the engine ships it
…ant names (update contracts too)
abrichr
force-pushed
the
feat/learn-excuses
branch
from
October 31, 2026 18:22
6b63680 to
a9b471b
Compare
This branch has not been deployed
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.
Stacked on #1678 (shadow mode). Large: about 2,700 lines of its own, 1,230 of them tests.
The problem
Many apps write to their own database on every request: the acting user's
last_login, an API token'slast_seen, a first-login property row for the acting user. The whole-database frame sees those rows move inside every write's window, nothing in the contract explains them, and it refutes every write, correct or not. In the held-out retest, three of four apps had every correct write refuted this way.The change: learn, then enforce
openadapt-flow learn(alsopython -m openadapt_flow.learnandopenadapt learn):collect: runs shadow mode (SQL) with a hook onShadow.on_checked. Each verifier call adds one line toepisodes.jsonl: the kind, the outcome, which parts held, and the frame's moved rows by table and column name. No record values.propose: writesproposal.yamland a readableproposal.mdwith a digest.approve: one command by a person writes the excuses into the contracts asframe.learned, recording the proposal's digest, who approved and when. All contracts of a kind or none. An edited proposal is refused.Fail closed at every step:
target(a row the write declares or adds) oractor_row(the users table's row keyed by the actor's id, or a row whose user column holds it; the user columns come from the schema's foreign keys or a person's--user-column). Any other row is listed as "stays a stop", however often it moves.{table, columns, change: changed|added, relation, actor_column}. Exactly the columns seen to move. Never a whole table, a wildcard, any row, or a removal. Proposed only when seen in at least K of every N learnable checks of that kind (defaults 3 of 5; each column must meet the same bar). Why: five is the smallest sample in which a majority still allows two checks where a throttled column (a last-used time written at most once a minute) didn't move, and a one-off isn't proposed. The count isn't the safety boundary; the relation is.frame.learnedinruntime/effects/learned.pyandframe.py): at each check and recheck the relation is derived again from this run's actor and declared rows. Atargetexcuse applies only to rows this write declares; anactor_rowexcuse only when the actor column holds this run's actor id before and after; the moved columns must all be in the excuse. A row in a table overmax_rows, whose baseline didn't keep every column, is never excused. Anything else is collateral, as before. The receipt names the excuses used and the proposal digest. Contracts without the key keep their digests.The verifier now also reports, for a refuted frame, each collateral row by names only (
evidence.frame.moves: table, change, columns, whether it's the write's own row, which columns hold the actor's id). The learner reads only that.Tests
tests/test_learned_excuses.py, on SQLite, and on MariaDB and PostgreSQL withOPENADAPT_SNAPSHOT_MYSQL/OPENADAPT_SNAPSHOT_POSTGRES:actor_rowexcuse never covers another user's row or token, another column of the user's row, a row moved to the actor, a removal, a row added for another user, an added row of a kind only changes were learned for, a column a large table didn't compare, or the same moves under another run's actor;targetexcuse never covers a look-alike row, another column of the target, or the excused column on a second row (the retest's fail-open, where a column excused for the target was excused on every row);--reject;A mutation check (scratch, not committed): letting any row count as the actor's fails 6 tests; letting any column count fails 2; letting a row count as the target fails 2.
Real-app evidence
Running.
Docs
packages/flow/docs/LEARNED_EXCUSES.md(new), pointers inVERIFY_RUN.mdandSHADOW_MODE.md, the command map and CAPABILITIES.Note on the diff
This branch merges
maininto #1678's branch to get the PostgreSQL frame (#1673), so until #1678 is updated frommainthe diff against it also shows #1672 and #1673.