notes 4 min css
Measuring AI-generated markup without guessing
You cannot detect that code was written by a model. You can measure a texture that generated code tends to have — and then the hard part is not flagging the careful hand-written page that happens to share it.
Start with the honest version: there is no signature. A model can produce markup indistinguishable from a person's, and a person can produce markup indistinguishable from a model's. Anybody selling certainty here is selling a coin flip with a progress bar.
What there is, is texture. Generated pages tend to carry several habits at once, and the habits are measurable even though none of them is wrong on its own.
The habits worth counting #
- Utility-class verbosity
- Forty classes on one element, the same spacing scale repeated at three different values, colour utilities that never resolve to a token.
- Structural padding
- A paragraph wrapped in eleven nested
divelements, each with one child, none of them doing anything. - Copy patterns
- Headings built from the same six adjectives, three-item lists everywhere, a call to action that says nothing.
- Accessibility theatre
aria-labelon an element that already has an accessible name, roles restating what the tag already means.- Runtime artifacts
- State written on every render, effects with dependency lists that cannot be right, the same fetch issued twice.
Any one of these is normal. A design system produces long class strings; a component library nests; a careful engineer in a hurry writes an aria-label that was not needed. The signal is the co-occurrence, not the habit.
<div class="flex flex-col gap-4 space-y-4 p-4 px-4 py-4
text-base leading-normal font-normal">
<div>
<div>
<p aria-label="paragraph" role="paragraph">
Unlock the power of seamless, cutting-edge solutions.
</p>
</div>
</div>
</div>
<!-- gap-4 and space-y-4 do the same job and fight.
p-4 is overridden twice by its own components.
Two wrapper divs hold one child each.
role="paragraph" is not a role.
The sentence names no product and no outcome. -->
Why the calibration matters more than the rules #
Any of those, on its own, is normal. A design system produces long class strings. A component library nests. A careful developer writes an aria-label because a screen reader needed it.
So the number that matters is not how many rules can fire, it is how the weighted total behaves on work that is unambiguously hand-written. The target was set the other way round from the obvious one: a tool that flags careful work is worse than no tool, because it teaches the reader to ignore it — and once ignored, it never catches the real thing either.
That is a harder direction to tune. Catching generated output is easy; leaving good work alone while still catching it is the whole engineering problem.
A score has to show its working #
A number with no method behind it is an opinion wearing a lab coat. Every finding says what it measured — the count, the element, the threshold it crossed — and offers a concrete change.
This has a practical consequence: the score is arguable. Somebody who disagrees can point at the rule that fired and say why it is wrong in their case, and sometimes they will be right. That is the design working, not failing. A verdict nobody can interrogate is one nobody should act on.
It also sets the standard for us. If a rule cannot be defended when somebody pushes back on it, the rule comes out.
What a score cannot tell you #
- Whether a model wrote it. There is no signature, and any tool claiming otherwise is selling certainty it cannot have.
- Whether the page is bad. A high score on a landing page that converts is a finding about maintenance cost, not about quality.
- Whether it was reviewed. Generated code that a person read, understood and kept is not a defect. The score measures texture, and texture survives review.
- Anything about the code you cannot see. This reads the rendered page. The repository behind it may be immaculate or may not exist.
The right reading of a high score is "this page has more of the habits than most" — and then the finding list, which is the part that is actually actionable.
What it is for #
Not for judging other people's work — although that is what it will get used for, and pretending otherwise would be silly.
The use that pays: run it on a page you just shipped fast. The findings are a refactor list ordered by how much they cost, which is the list you were going to write by hand anyway, on the day you had no time to write it.
Written by Ján Turský
Building LoupeKit and other browser tools out of Bratislava, under Apptiary.
tools in this note