// making the machine check the machine
…then something else is checking it.
What, exactly?
And who checks that?
Automated checks are what make it defensible to ship code you did not read line by line. Without them you did not move up a level — you stopped checking.
Part 1 — what to check, and in what order
Part 2 — how it actually works
// the top four are nearly free; do them first
A failing test → a red build. Push a fix
A leaked key → rotate it, and hope. Pushing a fix does not un-leak it
Order your checks by irreversibility, not by sophistication.
Ask it what a compiler cannot check. Never ask it whether the code compiles — a compiler answers that exactly, and a model guesses.
Review this pull request and tell me whether the code is correct.
Here is a diff. List only: a function or variable whose name no longer matches what it does; a docstring or README line this change made false; an edge case the change does not handle. If there are none, reply NONE.
If the model call errors, the run failed. It did not "find nothing"
A job that reviewed nothing must go red
Otherwise you get the worst outcome available: a green tick over a check that never ran.
Same input. Different output. Every time
Every testing instinct you have assumes determinism
assert summarise(article) == "Revenue grew 12%."
Stop asking "did this one call work?" Start asking "across a set of cases, how often is it acceptable — and is that number moving?"
One case tells you almost nothing
Twenty cases tell you whether your change helped
output == expected
Your feature summarises articles. Which do you build first?
Property checks are cheap, deterministic and repeatable
They catch the failures that actually happen: empty output, prompt echo, runaway length, missing the key fact
Give a model the output and a rubric; ask for a score and a reason
It works. It is also biased in known ways:
Is the feature better?
You improved the cases you were looking at
You have no idea what happened to the ones you were not
Without a measured set, prompt tuning is folklore. With one, it is engineering.
Twenty cases in a JSON file is a real eval suite
Do not build a framework. Build the file
// part 1 named the things. part 2 opens them up
Part 2 answers two questions: what actually happens when a run starts, and how you score an answer that is never the same twice.
The machine is not your laptop. No packages, no files, no keys — not even your code until a step fetches it
A step that exits non-zero fails the job, and the steps after it do not run
That emptiness is the point: the run can only pass on what is actually committed
name: ci on: [push] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 # 01: fetch the code - uses: actions/setup-python@v7 with: python-version: "3.12" - run: pip install -r requirements.txt # 02: it has nothing - run: python -m pytest # 03: the exit code decides
// it starts in the repository root, not in your folder
No module named …
No such file or directory
.env
- name: AI review run: python review.py || echo "review step finished"
review.py
||
echo
try: findings = review(diff) except Exception: findings = "no findings" # the error just became a verdict print(findings) # ...and the step exits 0
Make it go red on purpose once. A pipeline you have never seen fail is one you have no evidence works.
One output is a sample, not the answer. The same case can pass now and fail next run — that variability is what you are measuring
Twenty cases: one case is five points. A small move on one run is not yet a regression; the same case failing every run is
The reason string is what you read when the number moves
// below the judge, every rung is exact about shape and silent about meaning
The article says revenue rose 12%. The feature returns:
Revenue fell 12% this quarter, driven by renewals.
Your checks: non-empty · under 50 words · mentions "12%" · does not echo the prompt.
Every rung below the judge checks shape: is the token there, does it parse, how long is it
A reversed claim has perfect shape. This is the residue the judge exists for
Two honest options: a targeted property if the domain allows one — a direction word that contradicts the source — or a judge given the article and one question: is every claim in the summary supported by it?
Give the judge the source. Shown only the summary, it cannot check faithfulness — it can only rate how the sentence reads, and a wrong summary reads perfectly well.
Type this at whatever assistant you have. Then open a new conversation and type it again. Three runs in total.
Summarise this in one sentence: "The library moved study-room bookings online in March. Bookings rose 40% in the first month, mostly for evening slots, and no-shows fell by a third once reminders were added."
==
A support inbox. The feature reads a customer message and returns fields for the ticketing system:
{"product": "kettle", "issue": "stopped heating", "priority": "high"}
Different in kind from the summariser: some fields have one right answer
priority is a label from a fixed list → exact match. product must come from the message → a property. The whole thing must parse → structural
priority
product
Strongest rung the task allows — and this task allows higher rungs than a summary ever could
{ "id": "ticket-07", "input": "My kettle stopped heating after two days. I need a replacement before Friday.", "expect": { "keys": ["product", "issue", "priority"], "priority": "high", "product_in_input": true } }
Each case says what must be true of the output, not what the output must be
Twenty of these is an afternoon. It is also a real eval suite
def score(case, output): try: data = json.loads(output) # structural except json.JSONDecodeError: return False, "not JSON" for key in case["expect"]["keys"]: # structural if key not in data: return False, f"missing field: {key}" if data["priority"] != case["expect"]["priority"]: # exact return False, f"priority was {data['priority']}" if data["product"].lower() not in case["input"].lower(): # property return False, "product not in the message" return True, ""
Running 20 cases ticket-01 PASS ticket-02 FAIL not JSON: begins "Here is the JSON you asked for:" ticket-03 PASS ticket-07 FAIL priority was medium ... Pass rate: 16/20 (80%)
The commonest failure was not a wrong answer. It was a right answer wrapped in prose — and the parser downstream would have crashed on it
Two reasons, two different problems. The strings tell them apart without reading twenty outputs
Extract the product, the issue and the priority from this message.
Extract the product, the issue and the priority from this message. Reply with one JSON object and nothing else: no sentence before it, no code fence around it. priority must be one of: low, medium, high.
Then re-run the whole set, not ticket-02. Did a case that passed before start failing?
A stricter format instruction can cost you a field the model used to include. Only the set shows that
The pipeline does not know what an eval is. It knows that one step returned 1
So "what does red mean?" is your decision, in the script: below a floor, or below the last run
And the rule from part 1 still applies: if the feature could not be called at all, exit non-zero. Zero passes of zero cases is not 100%
Your eval set has passed every case on each of the last ten prompt changes.
Ten changes, ten perfect scores: you cannot tell the change that helped from the one that did nothing
A rate is information only if it can move. When it stops moving, keep the passing cases as the regression guard and add the case that fails today
The best new cases are real failures: the reversed figure, the JSON wrapped in prose, the run that dropped the number
The goal is not 100%. The goal is a number that goes up when you improve things and down when you break them.
Treating CI as administration rather than as the other half of the trade
Trusting green without ever having watched the pipeline go red
try
assertEqual
Twenty cases in a file, scored on every change, red when the number drops. That is the difference between engineering and folklore.