Skip to content

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:

  1. 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.
  2. 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.

  1. admin: create_product(price=1, stock=1)
  2. customer: buy(quantity=1)
  3. customer: refund(amount=1)
  4. 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), already is set to 0 instead of order.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.

Fix · app.py
--- 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:

  1. Exact replay. The counterexample's steps are replayed on the fixed app, and the rule must hold in every replay.
  2. Your project's tests. The fix must not break what already passed.
  3. The generated regression test. It failed before the fix and must pass now.
  4. 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 during rook 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.

Counterexamples and verified fixes · Roook