Docs
Counterexamples and verified fixes
A counterexample is the smallest sequence of valid actions that breaks one approved rule, replayed on the real app. This guide explains how Roook gets there and what counts as a verified fix, using one recorded case.
Minimal counterexamples
The search runs sequences of up to 12 steps, so the first sequence that breaks a rule usually carries steps and values that have nothing to do with the bug. The shrinker removes them in two passes:
- Steps. Delta debugging removes parts of the sequence and runs each shorter candidate for real, on fresh entities. A candidate is kept only if it still breaks the same rule. Steps whose requirements are no longer met are dropped with it.
- Values. Each param is moved toward its smallest or edge value for as long as the rule still breaks.
The result is minimal in a precise sense: take away any single step and the rule no longer breaks. Nothing is estimated; every candidate was executed against your running app.
Replay consistency
A sequence that failed once might have been luck or timing. So the engine replays the exact minimal sequence 10 times by default, on fresh entities each time, and reports how many replays broke the rule.
- Every replay breaks the rule, shown as 10/10: a deterministic bug.
- Some replays break it and some do not: the counterexample is marked flaky and still reported, with its count. Bugs that need two requests at the same moment usually look like this.
The counterexample is then saved as a JSON file together with a regression test. The test is run on the unfixed code first and must fail there, which shows it tests the bug.
The refund case, from a recorded run
Every value here comes from a recorded Roook run on minishop, Roook's own small test shop: one of the two example repos you can run with Try Roook.
Recorded example runBob's answers were replayed from a saved real run; the engine ran live against the real app.
1 · The rule
A rule you approved
Bob proposed it with evidence from the code. Each refund on its own looks valid, so no single request breaks it.
Total refunds on an order never exceed the amount paid
Must hold order.refunded_total <= order.paid
refunded_total_le_paid
2 · The smallest sequence
Four normal calls break it
The search broke the rule with paid 50 and refunded 60. The shrinker kept the 4 steps and cut the values to the smallest that still fail.
- admin: create_product(price=1, stock=1)
- customer: buy(quantity=1)
- customer: refund(amount=1)
- customer: refund(amount=1)
Observed after step 4
Rule broken
- paid
- 1
- refunded_total
- 2
3 · Replayed
Proved on the real app
The engine replayed the sequence on the running app and saved it as a counterexample plus a regression test.
- Reproduced
- 10/10
- Regression test
- test_rook_cx_001.py
On the unfixed code: exit 1: 1 failed, 13 passed in 0.44s
4 · Diagnosis
One line is the cause
The Detective's diagnosis, checked by the Diagnosis Reviewer against the evidence:
app.py:214
On line 214, in buggy mode (is_fixed=False),alreadyis set to 0 instead oforder.refunded_total.
5 · The fix
A one-line fix, reviewed
Written by the Surgeon in a workspace copy after approval, checked by the Fix Reviewer.
--- a/app.py+++ b/app.py @app.post("/orders/{order_id}/refunds") async def refund(order_id: int, body: NewRefund, user: CurrentUser) -> dict[str, int]: order = get_order(order_id, user)- # BUG 1 (buggy mode): each refund is compared with the amount paid on its own.- already = order.refunded_total if is_fixed else 0+ already = order.refunded_total if already + body.amount > order.paid: raise HTTPException(400, "refund exceeds amount paid") order.refunds.append(body.amount)6 · Verified
Four engine checks, all passed
Only the engine decides. The fix then landed on its own branch.
Exact replay passed
The same 4 steps, replayed on the fixed app.
rule refunded_total_le_paid held in 10/10 replays
Your project's tests passed
The app's own test suite.
exit 0: 13 passed in 0.37s
The generated regression test passed
The new test, with the project's tests. It failed before the fix.
exit 0: 14 passed in 0.44s
A fresh search, new seed passed
New random sequences against the fixed app. This run's budget was 1,500; the CLI default is 20,000.
no violation of 1 rules in 1500 sequences (seed 1:verify:1)
4/4 checks passedCommitted to branch rook/fix-cx-001
In this run the search first broke the rule with paid 50 and refunded 60. The shrinker tried shorter sequences, found that all 4 steps are needed, and cut the values down to 1. The fresh search in this recorded run used a smaller budget than the CLI's default.
Verified fixes
A fix counts only if four checks pass, and all four are run by the engine, not judged by an AI:
- Exact replay. The counterexample's steps are replayed on the fixed app, and the rule must hold in every replay.
- Your project's tests. The fix must not break what already passed.
- The generated regression test. It failed before the fix and must pass now.
- A fresh search with a new seed. No approved rule may break. In the CLI the default budget is 20,000 sequences or 120 seconds, whichever comes first.
When a check fails, Roook goes back to the diagnosis with the new evidence, up to 3 times, and then reports honestly that the fix is not verified. Only a verified fix is committed to a rook/fix-* branch.
The counterexample file
Every counterexample is saved as rook/counterexamples/cx_NNN.json, numbered from cx_001. It holds everything needed to replay the sequence exactly, and Roook never overwrites one that exists.
- version
- The file format, 1.
- cx_id
- The counterexample's id, such as cx_001. It is also the file name.
- rule
- The broken rule as it was when found: id, text, kind, scope, when and check.
- steps
- The minimal sequence. Each step has its action, actor and the exact params, plus refs: which earlier captured value each reference used. Steps sent at the same time are grouped under parallel.
- violated_at_step
- The step after which the rule was false, counted from 0.
- observed
- The values the engine read from your app at that point. Very large or deeply nested values are cut, and the cut is marked.
- expected
- The rule's check expression: what must be true.
- reproduced
- How many replays broke the rule, such as 10/10.
- flaky
- True when some replays broke the rule and others did not.
- seed
- The search seed the sequence was found with.
- model_hash
- A hash of the rook.yaml model it was found with.
- created_at
- When it was saved.
Replay, verify and explain
Three commands work on a saved counterexample, against an app you started yourself. None of them calls IBM Bob.
rook explain cx_001
rook replay cx_001 --base-url http://127.0.0.1:8000
rook verify cx_001 --base-url http://127.0.0.1:8000- rook explain
- Prints the rule, the minimal steps, the step where the rule broke, the observed values and the replay count. It needs no running app.
- rook replay
- Runs the exact steps against your app, 10 times unless you pass
rook replay cx_001 --base-url http://127.0.0.1:8000 --times 3. Exit code 1 while the rule is still broken, 0 when it held in every replay, 2 when the replay proves nothing or the arguments are wrong. - rook verify
- Runs two of the four checks on your app: the exact replay and a fresh search (2,000 sequences or 60 seconds by default; change them with
rook verify cx_001 --base-url http://127.0.0.1:8000 --sequences 20000 --seconds 120). Exit code 0 when both pass. Your project's tests and the regression test are not run by this command: they run inside the sandbox duringrook run.
- The counterexample is an id, looked up in rook/counterexamples/ of the current folder, or a path to its .json file. Pass
rook explain cx_001 --repo ~/.rook/workspaces/<run-id>to look in another folder, such as the workspace of a run on a local folder. - The model is read from rook/rook.yaml in the same folder, or from the file you name:
rook explain cx_001 --model ./rook/rook.yaml. - The app address must be a loopback address on your machine, such as http://127.0.0.1:8000. Anything else is refused.
- Values the model reads from the environment come from variables named ROOK_ENV_ plus the name, such as ROOK_ENV_ADMIN_PASSWORD.
In the web app, replaying a counterexample and downloading its test are not built yet.