← All posts
Engineering14 min read

The Great Inversion: AI Writes the Code, Production Pays the Review Bill

Generation got two orders of magnitude cheaper; human verification capacity did not move. GitClear's 2026 data shows duplication at its highest recorded level while refactoring falls — and the defects that reach production are structural, not syntactic.

Nati ShalomNati ShalomCo-founder & CTO
AI Writes the Code, Production Pays the Review Bill

TL;DR

  • The historic 80/20 split of engineering work — write code, review code — has inverted. Generation is nearly free; verification is the bottleneck. That is a throughput problem with a reliability consequence.
  • GitClear's 2026 research, tracking code-quality signals across 2023–2026, measures block duplication at 73.0 per million changed lines year to date in 2026 — 81% above 2023 and the highest on record. Copy/paste reached 15.7% of changed lines in H1 2026 while genuinely moved (refactored) code fell to 3.8%.
  • Duplication is not an aesthetic complaint. It is a propagation obligation: change one copy of a block and you inherit the duty to find every sibling, across files and domains you may not know.
  • The incidents this produces are a distinct class. They are not syntax errors — they are structural failures: correct-looking code that violates an invisible relationship between components. Tests pass, CI is green, and it fails only on real traffic in a real environment.
  • You cannot lint your way out of a structural defect. Detecting it requires knowing the system's actual shape: cross-repo contracts, live schemas, real IAM bindings, the deployed dependency graph.

The inversion, stated plainly

For decades the ratio was roughly 80% producing code, 20% reviewing it. In an AI-native organization that ratio has flipped, and the reason is arithmetic rather than ideological: generation got roughly two orders of magnitude cheaper, and human verification capacity did not move at all.

The consequences are measurable at both ends of the pipeline.

At the merge gate, review has become the constraint. Coding time compresses; review time does not. Teams are flooded with pull requests, and the human capacity to validate them for safety, efficiency and architectural correctness is unchanged. Survey data from Qodo and JetBrains found 40% of developers already spend two to five days a month wrestling with technical debt.

At the other end — production — the effect is more interesting and much less discussed. What actually reaches production is now shaped differently.

One developer, quoted in an arXiv study on AI-assisted development, put the reason better than any vendor deck:

I don't use it for code review, mainly because I need to understand what the code is doing myself [...] It's better for me to know each step of what it's trying to do, because I need to know ... how it's going to affect the rest of the system.

That last clause is the whole problem. How it's going to affect the rest of the system is exactly the knowledge a model generating a function does not have, and exactly the knowledge required to review the output responsibly. Which is why the review burden did not just grow — it got harder per unit.

What the code-quality data actually shows

The strongest public evidence here is GitClear's longitudinal work, which is useful precisely because it measures git history rather than asking developers how they feel.

The 2026 research — The Maintainability Gap — tracks seven code-quality signals across 2023–2026, spanning both risk behaviours (duplication, copy/paste, error-masking, churn) and reuse behaviours (refactoring, cross-file connectivity, legacy maintenance). The headline numbers:

GitClear's 2026 code-quality signals, 2023 through year to date 2026.
SignalFinding
Block duplication per million changed lines40.3 in 2023 → 73.0 YTD 2026 (+81%, highest on record)
Copy/paste share of changed lines15.7% in H1 2026
"Moved" (refactored) code shareDown to 3.8% of changed lines YTD 2026
Crossover point2024 was the first year on record where within-commit copy/paste exceeded moved code
Commits containing a duplicated blockRoughly 10× increase over two years

The moved-versus-copied inversion is the signal that matters most. GitClear treats moved code as one of the clearest markers of healthy refactoring and reuse — you relocate a thing because you intend to share it. Copy/paste is the opposite instinct: you duplicate a thing because sharing it would require understanding where it belongs.

A model has no reason to know where something belongs. Given a task, it produces a locally correct solution. Asking it to instead locate the existing helper, understand why that helper takes the arguments it takes, and extend it without breaking three other callers requires system knowledge it does not have.

GitClear's framing of the cost is the sharpest I have seen: a duplicated block imposes a propagation tax. When a developer changes one copy of a five-line block, they inherit the obligation to find and evaluate every sibling — across files and domains they may not know — and decide whether the change must propagate.

That obligation is unpriced at merge time and paid later, usually by whoever is on call.

The new incident class is structural, not syntactic

Here is where I want to be precise, because "AI code causes bugs" is a lazy claim and the interesting version is narrower.

Generated code is often better than human code at the level a linter can see. It rarely has typos. It handles the obvious error cases. It is formatted correctly. Static analysis is usually clean.

The failures that reach production have a consistent shape instead:

Correct-looking code that violates a relationship nothing automated can see.

The relationship lives outside the file — in a database schema, an IAM policy, a network boundary, an API contract owned by another team, a shared cache's key format, a message envelope another consumer parses. The generated code is internally coherent. Its assumption about the rest of the system is wrong.

This is why the standard defences all miss:

  • Linters and static analysis examine the file. The defect is between files, or between the file and a live system.
  • Unit tests assert the behaviour the author expected, and mocks encode the same wrong assumption the code does. If the code assumes an index exists, the test fixture will have one.
  • Integration tests in CI run against a seeded database with production-shaped data at 1/1000 the volume, and mocked cloud services. Both differences hide exactly this class.
  • Observability cannot catch it because nothing is broken until real traffic hits it. There is no signal to alert on, and afterwards the signal you get is a symptom several hops from the cause.

And the review step that would have caught it is the one thing the inversion made scarce.

Worked example: the uniqueness index that stopped existing

One incident, start to finish. A change of the kind an AI agent produces dozens of times a day, and the specific mechanism by which it becomes an outage.

The task. Add a channel field to the notification_prefs table so users can set preferences per delivery channel — email, SMS, push. Straightforward, well-scoped, exactly the shape of work that gets handed to an agent.

What was generated. A migration and a repository method. The migration adds the column and, reasonably, widens the natural key to include it:

-- migrations/0184_add_channel_to_notification_prefs.sql

ALTER TABLE notification_prefs ADD COLUMN channel VARCHAR(16) NOT NULL DEFAULT 'email';

-- widen the natural key to include channel

DROP INDEX idx_notification_prefs_user;   -- was: UNIQUE (user_id)

CREATE INDEX idx_notification_prefs_user_channel   -- note: not UNIQUE

    ON notification_prefs (user_id, channel);

Read it slowly. The old index was UNIQUE (user_id). The new one covers the right columns — and is not unique. A single word disappeared.

Nothing complains. The migration is valid SQL, applies cleanly, and the index it creates is a perfectly good index. The generated repository method is correct too:

def upsert_pref(self, user_id: str, channel: str, enabled: bool) -> None:

    self.session.execute(

        text("""

            INSERT INTO notification_prefs (user_id, channel, enabled)

            VALUES (:user_id, :channel, :enabled)

            ON CONFLICT (user_id, channel) DO UPDATE SET enabled = :enabled

        """),

        {"user_id": user_id, "channel": channel, "enabled": enabled},

    )

Why every gate passed.

  • Static analysis: clean. It is valid SQL and valid Python.
  • Migration review: the diff looks like a widening. Both index names appear, both reference sensible columns, and the word UNIQUE is absent from the new line rather than present-and-wrong — an omission is much harder to see in a diff than a change.
  • Unit tests: pass. They mock the session.
  • Integration tests: pass. Each test creates one preference row per user, so no conflict path is ever exercised. And ON CONFLICT (user_id, channel) requires a unique constraint on those columns to fire at all — but with fewer than two conflicting rows in the fixture, it never needs to.
  • Staging: fine for three days. Low traffic, no concurrent writes to the same key.

Production, day four. The mobile client has a retry on preference save. A user with poor connectivity toggles email notifications off; the request is retried twice. Without the unique constraint, ON CONFLICT has nothing to conflict on, so all three inserts succeed. That user now has three rows for (user_id, 'email').

The symptom does not appear at the write. It appears in the consumer:

notifications-worker  ERROR  sqlalchemy.exc.MultipleResultsFound:

  Multiple rows were found when exactly one was required

  query: SELECT * FROM notification_prefs WHERE user_id = %s AND channel = %s

  at notifications/prefs.py:88 in get_pref

The worker crashes, restarts, picks up the same message, crashes again. CrashLoopBackOff. The notification queue backs up. Alerts fire on queue depth and pod restarts — three hops from the cause.

Why diagnosis is slow. Every visible signal points somewhere else. The worker is crashing, so you look at the worker. The queue is backing up, so you look at the queue. The exception is in prefs.py:88, a file nobody has touched in five months — and it is, in fact, entirely correct code. Its assumption that (user_id, channel) identifies at most one row was true when it was written and is a reasonable thing to have assumed.

The actual cause is a missing word in a migration that shipped four days ago, in a different repository, reviewed and approved.

What finds it quickly. Not correlation across telemetry — the telemetry says "worker crashing," which is true and useless. What finds it is a structural question: which changes inside this failure's blast radius altered a constraint that live code depends on?

Scope the blast radius from declared state: notifications-worker → the tables it reads → the constraints on those tables → the migrations that changed them, over the deploy window. That yields one candidate — migration 0184, which dropped a UNIQUE constraint on a table read by a service in a different repository. The evidence chain is then a single sentence: a schema change removed a uniqueness guarantee that a downstream consumer's query depends on.

The generalizable rule — which is the durable output, more than the fix:

Reject any migration that drops a UNIQUE constraint when a live query in any repository selects on those columns expecting a single row. Require an explicit acknowledgement and a compensating de-duplication step.

Checkable statically, from declared schema plus a cross-repo query index. No production traffic required.

The pattern behind all of these

The uniqueness-index case is one instance of a repeating shape. Every version has the same three properties:

  1. Locally correct. Every artifact, read on its own, is defensible.
  2. Globally wrong. It violates a relationship with something outside the file — a schema, a policy, a contract, a network boundary.
  3. Silent until real conditions. The activating condition is concurrency, volume, a locked-down network, a retry, or a real permission — none of which exist in the environments the change was validated in.

Other instances of the same shape, worth having on a checklist:

  • Cross-repo contract drift. A field's meaning is narrowed in a producer; a consumer in another repo still parses the old form. Both repos' tests pass.
  • Permission narrowing by API change. Switching a call to a variant that requires a different IAM action — for example moving from a simple send to a raw-payload send that requires a distinct permission. Same client, same credentials, runtime 403. Tests mock the cloud provider, so they are green.
  • Network-boundary assumptions. Code that works from a laptop and from a permissive dev VPC, then hangs to the deadline in a locked-down VPC with no interface endpoint. The symptom is a timeout, which reads as a slow dependency rather than an unreachable one.
  • Duplicated logic drifting apart. Two copies of a retry helper; someone fixes a backoff bug in one. The other keeps hammering. This is the propagation tax coming due.

Notice that in every case the symptom is several hops from the cause, and the symptom class is ambiguous — a timeout, a 403, a crash loop, an elevated error rate. That ambiguity is why symptom-based tooling struggles here and why the useful question is always "what changed inside the blast radius," not "what is broken."

Where DataAgent fits

Everything above is true regardless of what you buy. What we built for this specific class:

  • A living topology graph (TIDE) built from control-plane state and code structure rather than request traces — cross-repo dependencies, live schemas, IAM bindings, network paths. This is what makes "which change altered a constraint live code depends on" answerable at all.
  • Cross-repo dependency verification. Because the defect class is defined by spanning a repository boundary, single-repo review is structurally unable to see it.
  • Structural root cause detection (SURGE). Causation over correlation: instead of surfacing signals that moved together, identify the specific, timed, attributable change inside the blast radius. In the worked example, that is the difference between "the worker is crashing" and "migration 0184 dropped a uniqueness guarantee."
  • Active de-duplication, blocked at local and PR levels — a direct answer to the propagation tax, applied at the gate rather than discovered later.
  • Publicly stated: 86% precision on bug classification and architectural anomalies, by synthesizing logs, dependency maps and historic commits; one-click tested hotfix once a fault is classified; and on the human-cost side, the review burden that AI-assistant workflows create is the same burden that shows up in our own 68% reduction in routine infrastructure maintenance effort.

The honest framing: this does not make AI-generated code safe. It makes the class of defect AI-generated code produces detectable, by supplying the system context the generating model never had.

FAQ

Does AI-generated code cause more production incidents?

The public data is about code quality rather than incident counts, so be careful with causal claims. What GitClear's 2026 research shows is that duplication is at its highest recorded level, up 81% since 2023, while refactoring signals have fallen — a maintainability trend that raises incident risk over time. The stronger claim I would defend is about shape rather than volume: the defects that get through are increasingly structural rather than syntactic.

Why don't linters or static analysis catch this?

Because they analyze a file, and the defect is a relationship between a file and something outside it — a live schema, an IAM policy, a network boundary, another repository's contract. Generated code is usually clean at the level a linter can see, which is precisely why clean static analysis gives false comfort here.

What is a duplicate code detection PR gate, and why does it matter more now?

A merge-time check that flags a new code block substantially identical to one already in the codebase, so the author has to choose reuse or justify the copy. It matters more now because copy/paste has overtaken refactoring in AI-assisted codebases, and each duplicate carries a propagation obligation: the next person to fix a bug in one copy must find every sibling.

How do you verify AI-generated code at merge without slowing everything down?

Tier the scrutiny by blast radius rather than by diff size. A fast pass for ordinary changes; a heavy pass reserved for anything touching auth, migrations, IAM, network policy, or shared contracts. In practice "how much scrutiny does this deserve" turns out to be the same judgement as "how much compute does this deserve," and both need a budget.

If tests and CI pass, what is actually left to check?

Whether the change is consistent with the live system, which is a different question from whether it is internally correct. Concretely: does it drop or weaken a constraint something else depends on; does it change a field's meaning across a repository boundary; does it require a permission the runtime role does not hold; does it assume a network path that exists in dev and not in production.

Is the answer to stop using AI to write code?

No, and I do not think that is available anyway. The answer is to move the verification burden from human attention, which does not scale, to a system that holds the structural context a human reviewer was using implicitly — and to notice that this is the same context an autonomous remediation system needs.

Key takeaways

  • The 80/20 split of writing to reviewing has inverted. Generation got two orders of magnitude cheaper; human verification capacity did not move.
  • GitClear's 2026 data: block duplication at 73.0 per million changed lines, 81% above 2023 and the highest recorded, while moved/refactored code fell to 3.8% of changed lines.
  • Duplication's real cost is a propagation obligation, incurred at merge and paid later — usually by on-call.
  • The resulting incident class is structural, not syntactic: locally correct, globally wrong, silent until real concurrency, volume, permissions or network boundaries apply.
  • Detection requires system shape — cross-repo contracts, live schemas, real IAM bindings, the deployed dependency graph. That is the same context an autonomous remediation system needs, which is not a coincidence.

See remediation-first in your own stack

Install an agent and watch DataAgent map your topology. No credentials, no commitment.

Keep reading

Essential Cookies keep the site working and cannot be switched off. Everything else is off until you turn it on.