DEV Community

Cover image for Debt is allowed. Lying about it is not.
Yucel Ozcan
Yucel Ozcan

Posted on

Debt is allowed. Lying about it is not.

The common wisdom: "Vibe coding produces rotten infrastructure."

That wisdom is not unfounded. AI produces plausible-looking code; as the project grows, the person having the code written slowly becomes unable to see, know, or read what is quietly breaking. And the existing tests were written by the very process that wrote the bug. Six months later, what you hold — like a giant war robot losing limbs one by one to enemy fire yet crawling onward into battle — is a system that shrinks as it grows, held upright only by the parts that still work. In projects run on a "surely someone is watching" ship-and-pray methodology, that end is inevitable.

This article's purpose is to show that it is not fate.

The field behind the claim

Behind the claim there is a field: an agentic call-center system, developed with vibe coding, answering real calls from real people in real time, currently running in a low-volume live pilot. Through a year of development, one principle never bent: post a guard at the door of every error class encountered, so the error can never repeat. None of the layers was designed in advance; each was born because something went wrong and was not waved through.

The guards standing watch in that system today fall into five groups:

  1. At commit time: if you changed code without changing a test, the commit is rejected.
  2. In CI, on your change: if less than 80% of the lines you touched are covered by tests, no merge.
  3. In CI, on the system's structure: anything that exists but is never reached gets hunted — configs never read, templates never rendered, route handlers never wired, dead code. Each is its own check, and each fails loudly.
  4. In CI, in the reverse direction: fixed something and forgot to remove it from the known-issues list? That is red too. Debt must shrink honestly.
  5. On dependency updates: a bot opens upgrades every week and CI is the referee; a conflicting version is caught before merge.

Together with the guards on infrastructure duty, eleven guards in total.

Breaking it on purpose

How much can that list be trusted?

Last week it was tested on purpose: on a throwaway branch, a forbidden dependency was deliberately added. The first thing that stopped it was the layer nobody expected — the commit hook rejected it before it ever reached CI. Imitating a hurried developer, the hook was forced past; this time CI went red in three separate checks at once. And one was a surprise: the added code had made a known issue in a completely different file invisible; the system said "your list records an issue that no longer exists — fix your list." It caught the probe from the direction almost nobody guards.

Probes like this carry their own trap, and the repo's "Proving the gate" section spells it out: scanners reason over your whole corpus, so an obvious probe can be silently absorbed. A probe name must satisfy two conditions at once — unique in the corpus, and not colliding with a name your baseline already records. Otherwise you stare at a gate that stayed green and walk away believing you proved something. A check you have never watched fail is a check you do not have.

Two records that rotted

Two observations from the other direction. While preparing this article, the system's test count was verified: the project's own documentation said 1,716; a count on a clean checkout came back 5,506. No tool caught that — no tool could have, because nothing was checking that number. The second is more striking: the number of protection layers was "known" to be nine; the count came back eleven. The record in the docs rots, and so does the record in your head. Every hand-maintained record rots; a record you do not want rotting must be checked by a machine. That is this article's entire point.

The part that could be packaged

These layers do not all come from one tool. Groups one, two, and five are off-the-shelf parts: the commit gate is a git hook, the coverage gate is a separate tool, and Dependabot stands the dependency watch. Anyone can set those up today.

The part that could be packaged and handed on is groups three and four: the scaffolding for building checks that audit your system's structure, and the incorrigible honesty of those checks' allowlists. The tool does not hand you the answer to "which config is never read"; it gives you the way to build the check that asks that question, and it never lets your answer rot. Its name is pytest-ratchet; it is shared on GitHub under the MIT license, for fellow travelers on the same road.

Because those lists break in the same place in every project: you fix the issue, you forget to delete the line; from that moment on, the list is lying. Better no list at all — a list like that pre-approves the next occurrence of the same mistake.

pytest-ratchet was built to keep that list honest and trustworthy. Precisely what it does:

  • A new finding that is not on the list turns the run red: debt cannot grow silently.
  • A finding that no longer has a real counterpart also turns the run red: the record cannot drift from reality.
  • Every entry on the list carries a reason field that cannot stay empty; because that line will outlive whoever wrote it, and "I'm sure there was a good reason" is not a reason.

The reason field has a nuance, and honesty requires stating it here too: "TODO" is a legal value. ratchet init seeds existing findings with a TODO reason — so in the first minute you hold a list whose reasons are unfilled, and the tool does not hide that: every run tells you how many TODOs remain and how old the oldest is. You fill in the reasons at your own pace; the impatient turn on strict mode, and from then on TODO is red too.

The tool's standing in its own field is two different stories. In the system it was born in, it runs two guards today: in one, it runs beside the hand-written mechanism it will replace — the results are identical, and the old mechanism retires after a few days of parallel running. In the other, there was a takeover: the architecture rule's hand-maintained exception list was removed, and that guard gained a staleness check it had never had. In one place the tool proves its fidelity; in the other, it completes the missing half.

What it does not do

Let me also say what it does not do: it does not fix your code quality, does not write your tests, does not decide which debt you should accept; it only keeps the list of the debt you did accept honest. Debt is allowed; lying about it is not — not even the innocent kind.

Its limits are stated as well: it keeps no numeric budgets (entries are matched as a set; "at most 16 exceptions may exist"-style measured debt is not modeled yet), entries have no tag field yet, and reasons live in a separate file from the thresholds they justify. All of it is written in the repo under "Known limits" — a tool that enforces honest records must keep an honest one about itself.

Prior art was surveyed before release; the results are in the repo under Prior Art, with sources. Bidirectional enforcement is not new: mypy-baseline, import-linter, and PHPStan, each within its own domain, reject stale entries by default. The missing piece was elsewhere: a written reason per entry exists in none of the surveyed baselines — some docs even call the "why" note crucial while giving it no place in the baseline. The combination claimed here is this foursome: a required per-entry reason field + breaking in both directions + working with any scanner's findings + running inside pytest. Nothing more is claimed. And at the head of the roadmap sits a cross-language bridge: running without pytest, with any scanner that can emit its findings as JSON — not today; it will be announced when it ships.

Try it

Trying it is three steps: install the package, say "ratchet init", run pytest — and from then on, on your own machine and in CI alike, a new finding is red and a stale record is red. The full commands and the docs are in the repo.

The cure for rotten infrastructure is of course not a package alone; the cure is the habit of never waving an anomaly through. This tool is the smallest piece of that habit that fits in a box — the rest is on you.

All of it is packed as provisions for fellow travelers on the same road. Contributions and feedback are welcome.

Debt is allowed. Lying about it is not.

GitHub: https://github.com/YucelOzcan/pytest-ratchet

Top comments (0)