Je codebase heeft twintig manieren om geld te tonen
6m leestijd

Je codebase heeft twintig manieren om geld te tonen

AI-code-duplicatie is stil: een overgenomen codebase met twintig inconsistente formatters voor geld en datum. Hoe bijna-klonen ontstaan, hoe je ze opspoort en de CLAUDE.md-regels die ze stoppen.

Gisteren werkte ik in een codebase die niet van mij was. Overgedragen door een andere ontwikkelaar die de agent volledig zijn gang had laten gaan, vibecoden op zijn ergst: model aan het stuur, niemand die de diffs las. Ik moest één functie aanpassen, één plek die een getal in een geldbedrag veranderde. Ik ging hem zoeken en vond er twintig.

formatMoney. toCurrency. displayPrice. formatAmount. priceLabel. Sommige rondden naar boven af, andere kapten gewoon af. Twee hadden een euroteken hard in de code staan, één leidde de valuta af uit de locale, één zette het teken achter het bedrag, één ervoor. Datums waren nog erger: bij een stuk of twaalf ben ik gestopt met tellen. formatDate, humanDate, dateStr, toDisplayDate, elk met een eigen idee over welke tijdzone je bedoelde.

Niemand heeft deze bewust geschreven. Niemand is gaan zitten met de gedachte dat de codebase een vijfde manier nodig had om een prijs te tonen. Ze stapelden zich op, sessie voor sessie, prompt voor prompt, elk logisch in de vijf regels eromheen en niet te verdedigen zodra je alle twintig naast elkaar zag.

Dit is wat AI met DRY doet. Niet met een klap. Met geheugenverlies.

De agent begint elke keer koud

Een agent kent je codebase niet. Hij kent het bestand dat je hem geeft en wat er verder in het venster past. Vraag hem een prijs te tonen en hij schrijft je een keurige kleine formatter, want er een schrijven is makkelijk en controleren of de repo er al een heeft is werk dat niemand hem heeft gevraagd.

Hij heeft geen herinnering aan de formatter die hij drie prompts geleden diezelfde middag maakte. Hij heeft geen wereldmodel van de repo. Elk verzoek komt binnen met een schone lei, en een schone lei lost elk probleem voor het eerst op.

Een mens doet dit ook, af en toe. Het verschil is dat een mens de kosten voelt. De functie schrijven is typen, en typen is wrijving, en wrijving laat je even pauzeren en denken "die hebben we vast al". De agent voelt niets. Hij produceert de eenentwintigste formatter net zo vrolijk als de eerste, in de tijd die jij nodig hebt om deze zin te lezen.

Copy-paste was er nog eerlijk bij

Vroeger maakten we ons zorgen over copy-paste-engineering. Die angst oogt nu bijna schattig.

Als een mens kopieert en plakt, dupliceert hij een bekende functie. Er is een origineel. Het heeft een naam waar je op kunt grep'en. Blijkt het fout, dan fix je die ene bron, en de kopieën zijn tenminste vindbaar omdat ze er identiek uitzien.

Waar een mens kopieert, genereert een agent. Elke kloon is nieuw: zelfde taak, andere naam, andere edge cases, andere bugs. Je kunt niet op formatMoney grep'en en de andere negentien vinden, want die heten toCurrency en renderPrice en amountString. Er is geen origineel. Er zijn twintig originelen.

Tel nu de afrondingen. Vier van die formatters rondden anders af. Vier verschillende getallen op vier verschillende schermen voor dezelfde bestelling. En een supportticket dat niemand kan reproduceren, want het hangt ervan af welke formatter welke pagina heeft gerenderd.

Duplicatie was vroeger een onderhoudsprobleem. AI-duplicatie is een correctheidsprobleem in het jasje van een onderhoudsprobleem.

Je merkt het pas als je gaat zoeken

Hier zit de val: niets hiervan komt boven terwijl je de diff leest.

Elke PR voegt één klein, logisch hulpje toe. De diff is groen. De tests slagen, want de nieuwe formatter wordt tegen zichzelf getest. De reviewer ziet vijf schone regels en keurt goed. De twintigste formatter komt de codebase net zo soepel binnen als de eerste, en dat is precies de rot die langs de PR-review glipt wanneer elke losse wijziging er prima uitziet.

Je vindt het zoals ik het vond: doordat je wilt veranderen hoe geld wordt getoond en ontdekt dat er geen "hoe geld wordt getoond" is, maar twintig, en dat de archeologie nu van jou is.

Dit is de stillere neef van meetbare code-churn. Churn kun je in een getal vangen. Bijna-klonen verstoppen zich, want elk ervan is technisch levende code die technisch werkt. Het is de lavalaag die zich één hulpje tegelijk vormt.

De bijna-klonen opsporen

  • Grep op werkwoord, niet op naam. Je vindt duplicaten niet aan hun namen, want hun namen zíjn het probleem. Zoek op wat ze doen: Intl.NumberFormat, toFixed, .getTime(), valutatekens, imports van datumbibliotheken. De aanroepplekken clusteren, ook als de functienamen alle kanten op vliegen.
  • Draai een clone-detector. Tools als jscpd, en taalspecifieke similarity-checkers, vinden structurele bijna-duplicaten die geen enkele grep vindt, juist omdat ze de namen negeren. Draai er een over een gevibecodete codebase en het getal ligt meestal hoger dan je gokt.
  • Let op de spreiding van imports. Vijf bestanden die date-fns importeren, drie die dayjs importeren, twee die het met de kale Date doen: daar vertelt de datumlogica je gratis, in de importlijst, dat ze versplinterd is.

De oplossing is geheugen, geen discipline

Je kunt je hier niet uit disciplineren, want het ding dat de duplicaten maakt draait puur op context. Dus je lost het op waar die context leeft.

Hier is CLAUDE.md voor: een korte, afgedwongen set "die hebben we al, gebruik hem"-verwijzingen, precies wat een stijlgids nooit weet te worden.

markdown
## Formatting: there is exactly one of each

- Money: `formatMoney(cents, currency)` in `src/lib/money.ts`. Do NOT
  write a new currency formatter. If it can't do what you need, extend it.
- Dates: `formatDate(date, style)` in `src/lib/date.ts`. All display dates
  go through it. Never call `toLocaleDateString` directly.

Before adding any `format*` / `to*` / `display*` helper: grep `src/lib`
for an existing one first.

Die laatste regel is de belangrijke. Je geeft de agent de zoekopdracht die hij uit zichzelf nooit zou draaien. Conventies die de agent niet kan zien bestaan net zo goed niet, en dat is precies waarom een ontbrekende of verouderde conventie je meer kost dan je denkt.

Perfect wordt het niet. Af en toe grijpt hij nog naar een verse formatter, en af en toe vang je er nog een in review. Maar de default kantelt van "verzin" naar "check eerst", en die twintig wordt niet elke sprint eenentwintig.

DRY ging altijd al om één plek in de codebase om gelijk te hebben. Minder typen was een bijverschijnsel. Eén plek, zodat je, als de afrondingsregel verandert, of de valuta, of de tijdzonebug eindelijk gemeld wordt, het één keer aanpast en klaar bent.

Met AI werd code produceren goedkoop, en dubbele code produceren net zo goedkoop. Die ene plek om gelijk te hebben werd stilletjes twintig plekken om subtiel ongelijk te hebben. Die twintig formatters waren niet van mij, en dat is precies het punt: de ontwikkelaar die ze genereerde heeft de twintig zelf ook nooit gezien. De agent maakte er elke keer één, geïsoleerd, voor het eerst, en ging door. De rekening belandt bij wie erin moet werken. Gisteren was dat ik.

De codebase onthoudt uit zichzelf niets. De agent evenmin. Die taak is voor wie er nog is om erom te geven.