Varför Gradr skrev 107 egna ESLint-regler, och hur de gjorde LLM:er till pålitliga bidragsgivare genom att låta kodbasen själv upprätthålla sina konventioner redan vid granskning.

De flesta utvecklingsteam har samma LLM-arbetsflöde: generera kod, ögna igenom den i granskningen, skeppa och hoppas. Code review och magkänsla. Det funkar tills det inte gör det — och det slutar fungera snabbare än någon tror.
Vi valde en annan väg. 107 egna ESLint-regler, ~50 000 rader regellogik, fördelade på 28 domänspecifika konfigurationsmoduler. Ja, att underhålla 50 000 rader lint-regler är dyrt. Men varje regel ersätter otaliga framtida granskningskommentarer, rättelser under onboarding och produktionsincidenter klockan två på natten. Reglerna betalar sig inom några veckor.
DX:s död (som vi känner den)
Vi har en skarpare tes: traditionell Developer Experience (DX) håller på att dö.
Standardreceptet för DX har alltid handlat om "Kolla hur lätt det är att bygga den här funktionen!". Komponentbibliotek, hjälpfunktioner, boilerplate-generatorer, low-code-plattformar — allt designat för att göra det enklare och snabbare för en människa att skriva kod. Hela frontend-ekosystemet ägnade ett decennium åt att optimera för färre tangenttryckningar.
Men om det är en LLM som skriver koden är frågan inte "hur lätt är det att skriva?" utan "hur lätt är det att verifiera?".
Det spelar ingen roll om koden är plågsam att skriva, om den som skriver den inte kan känna smärta.
De här reglerna är den nya DX:en. En människa skulle ha svårt att skriva ens en enkel funktion utan att bryta mot 5–10 av dem. Det har en LLM också — men reglerna ger den en tydlig väg att rätta sina misstag och landa i korrekt kod.
Regler som arkitektur
Alla som någon gång har använt en LLM för att generera kod känner igen det ständiga tjatet.
- "Nej, använd inte en rå Error-klass"
- "Använd vår befintliga API-klient i stället för fetch()"
- "Validera inte inuti route-handlern, definiera ett ordentligt schema vid gränsen"
Det vanliga är att bara slänga in ännu en kommentar i AGENTS.md. Med 107 regler (och fler för varje vecka) skulle det dokumentet bli en ohanterlig röra. I stället skriver vi en lint-regel för varje invariant. Varje gång vi behöver säga något till agenten, eller stöter på en bugg i produktion, stannar vi upp och frågar oss: "Skulle det här kunna upprätthållas statiskt?"
Att upprätthålla kodbasens konventioner
Ta ett enkelt exempel. I vår kodbas är det förbjudet att kasta new Error():
// ❌ This triggers no-new-error
throw new Error('Something went wrong');
// ✅ Must use typed errors with proper context
throw new Validation.Error('Invalid exam code', { examCode });TypeScript flaggar inte det här. Inte ESLints inbyggda regler heller. Men ett otypat Error innebär att vår felspårning inte kan kategorisera det, att våra API-handlers inte kan skilja användarfel från serverfel och att våra error boundaries inte kan visa meningsfulla meddelanden. En LLM skriver new Error() varenda gång om inget stoppar den.
Gammal DX säger: "gör felhantering enkel." Ny DX säger: "gör felaktig felhantering omöjlig att uttrycka."
Några till ur samma kategori:
no-fetch-in-server — LLM:er genererar ständigt serverhandlers som anropar samma server via fetch() för att återanvända logik. Det fungerar lokalt, går sönder i produktion och gör spårning omöjlig:
// ❌ Triggers no-fetch-in-server
async function createExam(input: ExamInput) {
const template = await fetch('http://localhost:3000/api/templates/default');
// ...
}
// ✅ Call the function directly
async function createExam(input: ExamInput) {
const template = await getDefaultTemplate();
// ...
}no-direct-ai-sdk-imports — Bara en enda fil i hela kodbasen får importera generateObject eller generateText från AI SDK:t. Allt annat går via en skyddad wrapper som hanterar tokengränser, rate limiting och audit-loggning:
// ❌ Triggers no-direct-ai-sdk-imports
import { generateObject } from 'ai';
// ✅ Use the guarded wrapper
import { generateObject } from '$lib/ai/client';Utan den här regeln importerar varenda LLM-genererad funktion SDK:t direkt — det är det vanligaste autocomplete-förslaget i deras träningsdata.
require-api-client — Alla API-anrop från klientkod måste gå via typade API-klienter. Inga råa fetch('/api/...'). Det ger typsäkerhet hela vägen och förhindrar att URL-strängar glider isär.
Vässa TypeScript-kompilatorn
TypeScript byggdes med mänskligt omdöme i åtanke. Språket har nödutgångar — any, as Type, @ts-ignore — som erfarna utvecklare använder sparsamt och av goda skäl.
LLM:er har inget sådant omdöme. De tar till any eller @ts-ignore vid minsta motstånd. Så vi förbjuder varenda nödutgång helt:
// ❌ All of these are lint errors in our codebase
const data: any = response.body;
const id = input as string;
const code = input as unknown as ExamCode;
// @ts-ignore
brokenFunction(wrongArgs);Om typerna inte passar fixar man typerna — man tystar inte kompilatorn. Mer friktion när man skriver, dramatiskt enklare att granska och underhålla.
I praktiken
I praktiken är de flesta projekt röriga, dåligt strukturerade och spretiga. Tro oss, vi av alla borde veta.
Våra första 6 månader såg ut så här:
- Ingen typning alls (ja, på riktigt)
- Python-backend i ett separat repo, utan dokumenterade API-kontrakt
- CI fanns inte
I dag har vi ett monorepo med 100 % TypeScript, 107 egna lint-regler fördelade på 28 domänspecifika konfigurationer, och en CI-pipeline där varje PR måste klara typer, lint, formatering, unit-/integrations-/e2e-tester, a11y och beroendeskanning.
Det här är förstås en grov förenkling — varje steg skulle kunna bli ett eget inlägg. Men ungefär så här såg migreringsvägen ut:
- Migrera allt till TS, maximal strikthet. Det här är grunden
- Börja med regler med hög avkastning som stoppar nödutgångar i TS eller enkla brott mot konventionerna
- Bygg CI som fallerar på typ- och lintfel
- Låt varje regel börja som en varning, och uppgradera den till fel när antalet överträdelser når 0
- Spåra alla varningar, och se till att ingen PR kan öka antalet varningar
Steg 3–5 är det som får svänghjulet att snurra. LLM:er kan åtgärda i princip alla lint-varningar på egen hand, och till och med avgöra vilka som är lämpligast att ta först.
För att ge det här tänder internt byggde vi en topplista som räknar åtgärdade lint-varningar per PR. Den är gamifierad — vår främsta bidragsgivare har just nu rangen "Grandmaster 1". Fånigt, men det funkar: varje PR för kodbasen framåt, inte bara funktionen.
Som allt annat är det en iterativ process. Alla regler överlever inte. Vissa visade sig vara för brusiga, vissa krockade med andra regler, och vissa upprätthöll mönster som vi senare växte ur. Vi har tagit bort eller skrivit om dussintals regler under det senaste året. De 107 som finns i dag är inte de 107 vi började med — det är de som har förtjänat sin plats.
Slutsats
Varje regel är en hel kategori av "Gör inte så här"-meddelanden som vi aldrig behöver skriva igen.
Resultatet? Under det senaste året har vi mer än tredubblat vår output per utvecklare. Vi delar de exakta siffrorna i ett framtida inlägg.
Den gamla DX:en frågade: "hur få rader krävs för att bygga det här?" Den nya DX:en frågar: "på hur många sätt kan man bygga det fel?"
107 regler är vårt svar. Och vi lägger till fler varje vecka.

