Quality that isn't negotiable: automated gates for AI-generated code
A 90% coverage gate, Psalm, PSR-12, HTML validation and CI: how I brought the standards from years of hand-written code to an AI-built project, and which bugs turned up along the way.
Part 3 of 3 in the series "AI development with guardrails". Part 1: specs before code · Part 2: context files
Before AI agents existed, I wrote on this blog about PSR-2 and about PHPUnit tests. A consistent style, tests for every feature, code a colleague can still understand two years later. For many years that was my craft.
With bauernjobs.de, which I have built almost entirely with AI since October 2023, I forgot about it for a while. The code came quickly, it mostly worked, and I checked it in the browser instead of with tests. At some point I asked myself: why don't my own standards apply just because a machine is doing the typing?
The answer was not to write the code by hand again. The answer was to write the standards down in a form a machine can check.
Why rules alone are not enough
The first two parts were about specs and context files. Both tell the agent what to do. Neither makes sure it does it.
AI agents have a well-known weakness: they want to finish. When a test fails, the shortest path to green is sometimes not fixing the bug but adjusting the test. A static analysis warning can be silenced with a comment. A threshold can be lowered in the config.
People do that too, but less often and with a guilty conscience. An agent does it with the same confidence as everything else. That is why the checks have to run outside the agent, on every commit, and the agent must not change them.
What gets checked at bauernjobs.de
The starting point in September 2026 was sobering. There were tests, and a CI pipeline since March, but the test environment no longer started locally, the frontend build failed, and CI ran on different PHP and PostgreSQL versions from production. Green tests in CI said little about what happens on the server.
In early October 2026 I rebuilt that in a few days:
1. The same versions everywhere. Local, CI and production run the same PHP and the same PostgreSQL version. A green test in CI now tells you something about production.
2. PSR-12 formatting, automatically. Laravel Pint checks on every commit through a Git hook and again in CI. The first run reformatted 93 files. Since then there is no discussion about style, and AI-generated code looks like the rest.
3. 90% test coverage as a hard limit. CI fails when line coverage drops below 90%. Not a recommendation: a red build.
4. Static analysis with Psalm. Level 5, with a baseline: existing findings are frozen, new code has to be clean. It gets better every day without first spending months on old debt.
5. The build has to match the code. The compiled CSS and JavaScript are committed. CI rebuilds them and fails if they differ from what was committed. Without that check, new Tailwind classes go missing in production whenever someone forgets to rebuild, and agents forget that a lot.
6. Browser tests with Playwright. For what a feature test can't see, starting with editing job listings.
What the tests found
The interesting part was not the configuration. It was the day the agent had to bring the missing coverage up to 90%. To test code you have to run it, and that exposed what had not worked for months:
- Unsubscribe links in marketing emails didn't work. People who wanted to opt out couldn't. That is not just annoying, it is a legal problem.
- Campaigns didn't go to every address of a contact, only to one.
- Verified users landed on a 404 page after logging in, because the redirect dropped the language from the URL.
- Guests were sent to the login page in the wrong language.
- Job alerts showed the title in the original language instead of the recipient's, on a platform whose core feature is translation.
- Payment webhooks without a signature were not cleanly rejected with an error code.
None of these bugs produced an error message. All were hard to spot in the browser. All were in code the AI had written and I had "tested" by clicking through it.
The rule that holds it together
The change that restored the bauernjobs.de test environment contains a sentence I now copy into every project: "No lowered checks."
For the agent that means:
- Thresholds are not lowered, not even temporarily.
- A failing test is not adjusted until it is clear whether the test or the code is wrong.
- New static analysis suppressions need a reason that I read.
- The Psalm baseline is not regenerated to make new findings disappear.
- A task is done when the pipeline is green, not when it is ticked off.
The same on this website
ghinea.net follows the same rules, with two additions. PHP Insights checks complexity and architecture against fixed floors and fails on known security advisories in dependencies. And every public page goes through the W3C validator, because a page with invalid HTML is read differently by search engines and screen readers than it looks.
About 350 tests, 90% coverage, Psalm, PHP Insights, HTML validation, all in CI on the production server's versions. The code behind this article went through those checks too.
What you can take from this
If your team develops with AI, don't start with the perfect setup. Start with these three steps:
- The same versions in CI and production. Otherwise you are checking the wrong system.
- A coverage threshold that breaks the build. Set it where you are today and raise it step by step. Never lower it.
- Static analysis with a baseline. Freeze old findings, forbid new ones.
AI makes code cheap. It does not make it cheap to find out whether the code is right. That is the job of checks that see every commit and can't be talked round.
Not sure where your codebase stands on quality, whether it was written by hand or with AI? My legacy audit gives you a prioritised list in two weeks: what is critical, what can wait, and which checks keep it safe for good. Book a 30-minute call