Technical debt from AI coding: how to measure it
AI does not produce more technical debt so much as a different kind, one the usual measurements were not designed to see.
Technical debt was defined as a deliberate trade: take a shortcut now, pay interest later, and repay when you can. The definition assumes somebody chose.
AI-generated debt breaks that assumption. Nobody chose. It accumulates as a side effect of changes that each looked correct, which is why the standard measurements keep reporting that everything is fine.
How it differs
| Human debt | AI debt | |
|---|---|---|
| Origin | A deliberate shortcut | An incorrect inference |
| Someone knows about it | Usually, and often it is in a comment | No |
| Shape | Ugly code that works | Clean code in the wrong place |
| Typical form | A hack, marked as one | A duplicate, or a crossed boundary |
| Found by | Reading it: it looks wrong | Nothing, until it causes an incident |
That third row is the one that defeats existing tooling. Traditional quality tools look for code that looks bad: long functions, deep nesting, high complexity. Generated code scores well on all of it. It is well-formatted, consistently named, and reasonably decomposed. It is also, sometimes, the second implementation of something that already existed.
Four signals that actually work
These are structural rather than stylistic, which is why they catch what the usual metrics miss.
1 · Duplicate responsibility
Count the places that do the same job: validate the same thing, format the same value, call the same external service. Not copy-pasted text, which clone detectors already find, but semantic duplication: two different implementations of one responsibility. This is the single most reliable indicator of generated debt.
2 · Boundary crossings
Count how many distinct components reach a given resource. If eleven places query the database directly, you no longer have a data layer. You have eleven, and any schema change is now a repository-wide search.
3 · Orphan rate
The proportion of code nothing reaches. Orphans mean changes routed around existing code rather than modifying it, which is the characteristic signature of an agent that could not find the right place and made a new one.
4 · Map-to-code divergence
If you have a stated architecture, the count of things in the code with no counterpart in it, and vice versa. This is architecture drift expressed as a number, and it is the most direct measure available.
Metrics that mislead here
- Test coverage. Generated code often comes with generated tests, which can be high-coverage and assert almost nothing meaningful. Coverage measures execution, not verification.
- Cyclomatic complexity. Models write short, shallow functions. Low complexity with the logic spread across four duplicated paths is worse than one honest function.
- Lines of code. Volume is no longer a proxy for anything. It costs nothing to produce and says nothing about coherence.
- Static analysis warnings. Genuinely useful for catching real bug classes, and blind to the structural problems above by design.
Keeping it visible
Debt you can see is manageable. The specific danger of the AI variety is invisibility. Nobody made a choice, so nobody has it on a list.
Measure structure on every change, not quarterly
The four signals above are cheap to compute and only useful as a trend. A number that appears in a review once a quarter is a report; a number that moves on every pull request is a control.
Treat a new boundary crossing as a review comment
Not a build failure necessarily, but a change that adds the twelfth direct database caller should say so out loud, because nobody reading the diff will notice on their own.
Give duplication a home
When the check reports two implementations of one responsibility, the resolution is a decision: merge them, or state why both exist. Either is fine. Silence is not.
Keep the intended architecture written down
You cannot measure divergence without something to diverge from. This is the prerequisite for the fourth signal and most of the value of the other three.
Graphlit computes these structurally: the graph knows which nodes own which files, so duplicate responsibility, orphaned code, unbuilt nodes and boundary violations are queries against a map rather than an afternoon of reading.
- gate_not_wired
Auth check exists but nothing calls it
lib/auth.ts → app/api/payments/route.ts
- stub_shortcut
Refund handler returns success without refunding
app/api/refunds/route.ts
- duplicate_endpoint
Two handlers answer POST /api/checkout
app/api/checkout/route.ts
- orphan_file
Nothing imports this module
lib/legacy-cart.ts
- isolated_node
Receipts is drawn but connects to nothing
graph node · service:receipts
Hypotheses until an agent pass confirms them. A scanner that overstates its confidence gets ignored.
Common questions
Does AI-generated code create more technical debt?
Not necessarily more, but a different kind. Human debt is usually a known shortcut; AI debt is an unnoticed inference: a duplicate implementation or a crossed boundary. It is harder to manage mainly because nobody knows it is there.
How do you measure technical debt in AI-generated code?
Structurally rather than stylistically: duplicate responsibilities, how many components reach a shared resource directly, the proportion of unreachable code, and divergence between the stated architecture and the actual one. Traditional quality metrics score generated code well while missing all four.
Is test coverage a good measure of AI code quality?
Weakly. Generated tests can achieve high coverage while asserting very little, because coverage measures which lines execute, not whether anything is verified. A better check is whether the suite fails when you deliberately break the logic.
Draw it. Then keep it true.
Graphlit turns an architecture drawing into a typed graph, builds against it, and proves the code still matches. Free to start.