feat(verdict): add executed-without-online-safety contract - #108
Conversation
Keep future blocking execution fail-closed by admitting only typed refusal shapes whose lock risk can be meaningfully bounded. Distinguish one-relation index maintenance at the parse-only gate so broader forms remain ineligible.
Preserve typed refusal identity when an operator accepts bounded blocking work. Expose plan eligibility and a distinct outcome and exit code so automation never mistakes this path for online-safe success.
|
🤖 Adversarial review response — created by Kiran's code review agent (Amp, Claude Opus 4.6) — pull/108, follow-up commit All three findings are fixed in the follow-up commit, which also merges
Decisions to veto
Source: #108, scratch review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
🤖 1/2 — the contract and its coverage. The invariant registry, the docs, and the stack are in 2/2. Reviewed
Two things here are better than the PR describes them.
The two equivalent survivors are worth naming as such rather than listing as gaps. Dropping the 1. The audit record cannot be checked against the registry for the site-keyed row
I ran both eligible rows through the round trip that an audit consumer performs:
The emitted record for the first row is exactly the one this PR publishes at {"outcome":"executed-without-online-safety","reason":"index-statement","class":"by-design",
"blocking_passthrough":true,"lock_timeout":"3s","statement_timeout":"10m"}Nothing in it distinguishes the eligible RF-8 anticipates half of this and forbids the wrong fix: "Accepted-blocking eligibility is therefore consumed only from the proof of the verdict the same front-door invocation produced, never from a decoded verdict." That rule is right, and it is why a decoded verdict must never authorize anything. But it was written about eligibility as a decision input, and this PR introduces the verdict as an output — the audit record of an override that already happened. Those are different obligations: the input must not be forgeable, the output must be complete. Keeping the site off the wire satisfies the first by sacrificing the second, and the tension dissolves once they are separated — recording So the choice is between two defensible states, and the PR should pick one:
The current state is the third thing: a record that looks self-describing, with a comment citing an invariant that covers the in-process half only. 2. The renderer parity lock reports coverage it does not have
But the fixture's outcome is Three mutations demonstrate it. Deleting the budgets line from That means the entire new text rendering — headline, severity, the The fix is one entry, not a new harness: add a fifth verdict with 3. Exit 3 is the headline of the contract and nothing tests itChanging That matters more here than the usual " Smaller notes
This review was generated by Claude Code (claude-opus-5). |
|
🤖 2/2 — the invariant registry, the wire contract, and the stack. The code and its coverage are in 1/2. This change adds a terminal outcome, a process exit code, a versioned wire field, and a third site that consumes accepted-blocking eligibility — and it touches 1. RF-8 gains a third enforcement site and RF-7's registry pointer now resolves past the code
RF-7 has the sharper version of the same problem. The registry's own preamble is what makes this matter: it says this doc states "where each rule is enforced," so an The move itself is right and I said so in 1/2 — 2. A new terminal outcome and a new exit code arrive with no entry
The rule writes itself from
Written that way it also names what 1/2's finding 3 shows is untested, which is the usual value of adding the entry in the same PR. The If the intent is that the whole family lands together at step 4, the rollout section should say so explicitly rather than leaving the outcome undocumented in the registry between now and then. 3. The published console block runs flags that do not exist, and its JSON did not come from the pipeline
$ pg-sprite migrate --alter 'DROP INDEX public.users_email_idx' --accept-blocking public.users --lock-timeout 3s --statement-timeout 10m --jsonThe prose two lines above is honest that "nothing produces this outcome until It also quietly pins a flag spelling — 4.
|
aparajon
left a comment
There was a problem hiding this comment.
🤖 Approving. Reviewed in two comments above — the contract and its coverage in 1/2, the invariant registry and wire contract in 2/2. Fifteen mutations plus a round-trip probe of the audit record; full unit suite green, lint clean, CI 15/15.
provenRefusal is the right fix in the right place, and moving the eligibility registry into pkg/verdict is more than a file move — it is what lets an executor ask the question at all, which is the dependency direction #107 needed.
Four findings: the JSON audit record cannot be checked against the registry for the site-keyed row (site is not on the wire, and the decoded verdict re-derives as ineligible with no error — the published example is exactly that row); the renderer parity lock sets the three new fields on an OutcomeFailed fixture, so neither renderer emits them and the whole new text rendering is unpinned in both; exit 3 has no test though "cannot be confused with exit 0" is the contract's headline; and no docs/invariants.md change, which leaves RF-7's registry pointer resolving to a file the accepted-blocking half just left.
This stamp was left by Claude Code (claude-opus-5).
|
🤖 Adversarial review response — created by Kiran's code review agent (Amp, Claude Opus 4.6) — pull/108, follow-up commit All three findings are fixed in the follow-up commit, which also merges
Source: #108, scratch review |
Adds the verdict and plan-report contracts for operator-accepted blocking execution.
Why
Operators and automation need to distinguish bounded execution of an accepted refusal from online-safe success without parsing prose, and a dry run needs to say which refusals could later be accepted.
What
executed-without-online-safetyoutcome with retained refusal identity (reason,class,cause,safer_idiom), explicitlock_timeout/statement_timeout, theblocking_passthroughaudit field, and exit code 3; the constructor rejects a missing identity, an ineligible refusal, or a non-positive budget, and stamps the in-process refusal proof (RF-8) the same wayWithRefusaldoes.pkg/verdictso plan reports can consult it without importing the front door; the front door's completeness, fail-closed, class-keyed, and pinned-rows tests still cover every registered key.blocking_passthrough_eligibleto every refused dry-run statement (absent on non-refused statements) and bump the plan report format version to5per its documented policy;demo/tour.shasserts the new version.causeinto the JSON, so a partitioned-parent refusal reportsparent-blocking-index-buildand the identity round-trips throughRefusal().Causeis documented as the refusal-wide discriminator (budget fired, or parent shape refused), not a budget-only vocabulary.docs/cli-output-examples.md; the design doc's text example follows the renderers' field order.Nothing produces the new outcome yet; the executor and the flag land in later PRs.
Before / after
One statement,
DROP INDEX app.orders_created_at_idx, first as a dry run and then executed by an operator who has accepted the blocking refusal.Automation that treats exit 0 as "online-safe success" keeps that meaning; the accepted-blocking path is a distinct outcome and exit code it must opt into.
🤖 Drafted with Amp (Claude Opus 4.6); reviewed and edited by the author.