Rules as tests: write the rule once, run it on every commit

The pattern in one picture
A test checks a piece of code. You write the code, then you write its test, one by one.
A codebase also has rules: "entities in a test reuse the slice's mocks", "every failure case asserts its error code". Those used to live in a document and in a reviewer's head. Now each rule becomes a test of its own. You write it once, and it runs against every file in its scope, including the ones an agent writes tomorrow.
Before
One piece of code, one test
code A
its test A
code B
its test B
code C
its test C
Every file needs its own test, written by hand.
Now
One rule, one test, every file
The rule
“Entities reuse the slice’s mocks”
Its test
written once
- user.test.ts
- order.test.ts
- route.test.ts
- the file an agent writes tomorrow
Every commit, every push, every CI run, in under 500 ms.
That is the whole idea. The rest of this post is how it works, what the test looks like, and why it changes the cost of reviewing AI-written code.
Why I needed it
My AI agents commit through a gate of guardians: reviewer subagents, one per area of the codebase, that read the staged diff and grade it.
Guardians are good reviewers, and they are paid on every pass. A one-line fix reopens the round, because any edit in a guardian's area voids its score. Plans that should take minutes took an afternoon.
Most of what a guardian checks is a single judgement over a single file. That kind of question needs a model that reads the file, and only the file.
Jev, in two lines
Jev is TypeSafe's evaluation model. You give it a context and a set of yes/no questions, and it returns the probability of yes for each one, at the pace of a linter, charging only for input.
How it works
One run, per file
under 500 ms- 1
The rule
A declarative file in the repo: what it looks at and what it asks.
- 2
The pieces
The test file, the module it tests, the mocks it could use. Built once per file.
- 3
Scope, decided in code
If the file has nothing the question judges, it is skipped with its reason.
- 4
One call to Jev
All the rule’s questions at once. One probability per question.
- 5
The verdict
Each probability against its barrier. A red names the file, the rule and the question.
- 6
The record
Stored by content hash. The commit gate reads it offline.
A rule is a declarative file that lives in the repo. It says what to look at and what to ask:
export default {
title: 'The test reuses the slice’s mocks and leaves them untouched',
on: 'src/**/*.test.ts',
// The pieces of context Jev receives for each file.
state: ['file', 'subject', 'mocks'],
questions: {
reuses_slice_mocks: {
// Below this probability the test fails. Set from a real red and a real green.
barrier: 0.6,
// Code decides who gets asked: no mocks, no question.
applies: { mocks: true },
instructions:
'Does every entity-shaped object in `file` ' +
'name an imported mock or spread one?',
},
},
}The engine builds the pieces for each file (the test, the module it tests, the mocks it could use), skips the files outside the question's scope, and sends Jev one call per rule with all its questions.
What the test looks like
The test reuses the slice’s mocks
reuses_slice_mocks · below the barrier
Does every object in `file` shaped like a domain entity either name an imported mock or spread one?
It is a plain Vitest suite: a describe per file, a test per rule, a row per question. A red names the file, the rule and the exact question that failed, so whoever reads it (a person or an agent) knows what to fix.
Built once, runs everywhere
Built once
The rule’s test
A Vitest suite: a command, an exit code, rows on stdout
Runs everywhere
Pre-commit hook
Blocks the commit on a red
Push
Checks the whole branch
CI/CD
Same command, same exit code
The agent, while it writes
Checks its own test before asking to commit
A sweep of the tree
Audits old code against a new rule
The rule's test is built once and runs in the pre-commit hook, on push, in CI/CD, in a sweep of the whole tree, and inside the agent's own loop while it writes code. Each run takes under 500 ms.
Results are stored by content hash. The commit gate only reads that record, so it works offline and instantly, and the same file with the same questions is asked exactly once.
What it saves: time, tokens, rounds
Before
Review by a guardian agent
A reasoning subagent per area, on every commit
- git commit
- The gate denies: not reviewed
- Spawn the guardianagent
- Load brief, read diff, walk the repominutes · tokens
- Grade and plan of fixestokens
- Fix one line
Any edit in its area voids the score: the round starts over.
Now
The rule’s test with Jev
One yes/no question per rule, per file
- Edit the test
- Run the ruleunder 500 ms
- The red names file and questioninput tokens only
- Fix one line
- Green, recorded by hash
The agent closes this loop alone, before it ever asks to commit.
The difference is where the loop closes. With a guardian, each fix goes back through a reasoning agent. With the rule's test, the agent that wrote the code runs the rule, reads the red, fixes the line and runs it again, all before it asks to commit.
Time
With a guardian on every commit
A round of minutes per pass
With the rule’s test
Under 500 ms per run
Tokens
With a guardian on every commit
A reasoning agent that reads its brief, the diff and the repo
With the rule’s test
One input-only call per file, and zero for content already answered
Rounds
With a guardian on every commit
Every fix in its area reopens the review
With the rule’s test
The agent fixes and re-checks alone, before the commit
Harness loops
With a guardian on every commit
A fixed toll on every commit
With the rule’s test
The guardian is summoned by a red row, with that row as its brief
Why the architecture works
In the repo
reviewed in PRs, like any code
rules/<scope>/*.ts
what to look at, what to ask
In the engine
outside the repo, evolves on its own
Vitest runner
a test per rule, per file
Jev, via the AI Gateway
one call per rule
Record by hash
the gate reads it offline
- 01
The rule is data
Scope lives in
appliesandunless. A false alarm on a class of files is a one-line diff. - 02
Reviewed like code
Rules live in the repo, so changing a rule is a PR with its own review.
- 03
One engine, every scope
Tests, mocks, docs: a new scope is a new folder of rules, with the engine untouched.
- 04
Asking is separate from blocking
The gate reads a record keyed by content hash: offline, instant, reproducible.
- 05
Vitest for free
Exit codes, filters, bail and reporters come with it, and so does CI.
- 06
Calibrate before it blocks
observeprints the answer next to its barrier without failing anything.
The key design choice is the split. Rules live in the repo and get reviewed like code; the engine that runs them lives outside and evolves on its own. Adding a question is one entry, adding a scope is a folder of rules, and a false alarm is fixed with one line of scope.
Where the line is
Jev catches it
One judgement over one file
- Could a change in the code make these assertions fail?
- Does every failure case assert its error code?
- Does the test assert the calls, as well as the value?
- Is every entity an imported mock, or a spread of one?
Code does it better
Compare, count, cross-reference
expect.assertions(N)against the real count- Does
beforeEachrestore every mock a case overrides? - Would each case pass in any order?
- Does the test connect to a real database?
Jev is strong at one judgement over one file. Anything that compares two lists, counts, or depends on order belongs to plain code. The rule of thumb that came out of calibrating: code enumerates, Jev judges one thing at a time.
How to start
- List what your reviewer checks. Every guardian, every review checklist.
- Sort each item into code (a grep, an AST check), one judgement over one file, or real reasoning.
- Write the middle bucket as rules. One yes/no question each, with the mechanism visible in the text.
- Calibrate each question with a real red and a real green before it can block anything.
- Wire the suite into the hook and CI, and keep the reviewer agent for the last bucket.
The reviewer agent stays, and it gets called for the questions that need reasoning, with a red row as its brief. Every other rule becomes a test you write once.