Someone gave me read access last month to a product built almost entirely with an AI assistant. Node, TypeScript, roughly 30k lines, six months old, paying customers. The question was not about architecture. It was much more practical: why does changing one sentence in an email take a week?
The ticket behind the call was tiny. Reword the order confirmation email and fix the line that shows the applied discount. The diff that came back touched 12 files.
I did not read the codebase. I ran one search.
Start from the number, not from the folder tree
Business rules leave fingerprints, and the fingerprint is usually a literal: a threshold, a rate, a status string. Here it was a 15% discount above a 500 subtotal.
rg -n --no-heading '\b500\b' src/ | wc -l
# 19
rg -l '0\.15|0\.85' src/
src/pricing/calculateTotal.ts
src/services/checkout.ts
src/emails/orderConfirmation.ts
src/reports/monthlyRevenue.ts
src/admin/orderPreview.ts
Five files carrying the same rule. Four minutes of work, no context needed about the domain.
Then I opened them side by side, and the copies were not identical:
// src/pricing/calculateTotal.ts
if (subtotal > 500) {
total = subtotal * 0.85
}
// src/services/checkout.ts
const discount = subtotal >= 500 ? subtotal * 0.15 : 0
// src/reports/monthlyRevenue.ts
const discounted = orders.filter((o) => o.subtotal > 500)
At exactly 500 the checkout applies 75 off and the pricing module applies nothing. The report counts that order as undiscounted either way. One boundary, three answers.
I asked how often that happens:
select count(*) from orders where subtotal = 500.00;
-- 23
23 orders since launch got their total decided by whichever code path answered first. Nobody had opened a bug, because both numbers look plausible on a screen.
The tool I expected to catch this caught nothing
My first move was the obvious one, and it failed. I ran a copy paste detector:
npx jscpd src --min-tokens 50 --reporters console
# Clones found: 3
# Duplicated lines: 1.4% (all inside __fixtures__)
1.4%, and every hit was test fixture noise. By that metric the repo looks clean.
The reason took me a while to accept. Clone detectors are built for code that was copied. This code was never copied, it was regenerated. Someone opened a new session, described the discount again in slightly different words, and got a fresh implementation with a different variable name, a different comparison operator and a different shape. Token level similarity is low. Semantic duplication is total.
That is the part I had not internalized before this repo. Old style duplication announces itself, because the two blocks read the same. Regenerated duplication hides, because each version reads like it was written by a different person on purpose.
Git says how it got there
git log --format='%ad %s' --date=short -- \
src/pricing/calculateTotal.ts src/services/checkout.ts \
src/reports/monthlyRevenue.ts | head
2026-03-12 feat: order total with tier discount
2026-03-29 feat: checkout summary
2026-04-17 feat: monthly revenue report
2026-05-02 fix: discount line on confirmation email
Four weeks apart, one author, four separate sessions. Every commit was correct on the day it landed. None of them was wrong in isolation, and that is exactly why review never flagged anything.
The ten minute check that says whether the tests are real
Before touching anything I broke the rule on purpose. Changed the threshold from 500 to 5000 in the pricing module and ran the suite:
npm test
# Tests: 118 passed, 118 total
# Time: 11.4 s
118 green tests while pricing is wrong. Coverage was reported at 71%, so the number on the badge was fine. The tests exercised the functions, they just never asserted on the money.
What I did instead of proposing a rewrite
A rewrite was the first thing suggested on the call, and it was the most expensive option on the table. Three cheaper steps went in first.
One, a characterization test on the boundary, written before any refactor, pinning the behavior we decided was correct (>=, discount applies at 500):
describe('tier discount boundary', () => {
it.each([
[499.99, 0],
[500, 75],
[500.01, 75.0015],
])('subtotal %p gives discount %p', (subtotal, expected) => {
expect(discountFor(subtotal)).toBeCloseTo(expected, 4)
})
})
Two, one exported discountFor in src/pricing, then deleting the four copies one at a time, running the new test between each deletion.
Three, a crude guard in CI so the sixth copy does not get generated next month:
#!/usr/bin/env bash
# ci/check-pricing-literals.sh
hits=$(rg -l --glob '!src/pricing/**' --glob '!**/*.test.ts' '0\.15|0\.85|\b500\b' src | wc -l)
if [ "$hits" -gt 0 ]; then
echo "pricing literal outside src/pricing:"
rg -n --glob '!src/pricing/**' '0\.15|0\.85|\b500\b' src
exit 1
fi
It is blunt and it produces false positives, which we handle with an allowlist file. It also caught two new attempts in the following weeks, both from fresh AI sessions that had no idea the rule already existed. The next copy change on that email touched 2 files instead of 12.
Where this does not apply
If you are three weeks from finding out whether anyone wants the product, skip all of it. A prototype does not need a single source of truth for a discount, it needs proof that someone will pay. The moment there are real orders in a database and a boundary that decides money, five phrasings of the same rule stop being a style problem.
I also do not know how this scales past one rule. Grepping literals works because pricing hides behind numbers. Rules expressed as prose in a status machine, or as a chain of booleans, do not leave that fingerprint, and I have no cheap check for those yet.
So the honest question for whoever has been through this: what do you use to catch the same rule regenerated in different wording, when clone detection is blind to it? Structural patterns with ast-grep, embedding search over the codebase, architecture tests that fail when a module reaches for something it should not know about? I would rather learn a method than write more grep scripts.
Originally published on the Revin blog: https://revin.com.br/en/blog/building-with-ai-without-architecture
Top comments (0)