Motstykket til AURA, der ingen leser koden i det hele tatt

Pre-review-intervju

I team er koden ikke min alene. Flere er involvert, styringen må være god, og alle skal kunne forstå hva som foregår. Da holder det ikke at appen virker.

Så før jeg leverer fra meg en PR, intervjuer Claude meg om min egen diff. Ett spørsmål om gangen, både hvordan koden virker og hvorfor den ble slik. Håndvifting godtas ikke. Grønt lys først når forståelsen sitter.

Det er en sperre, ikke et tips

Draft-PR først, så intervjuet, og først da ut av draft. En draft er ikke delt med andre — reviewere varsles først når den forlater draft. Vakler noe på slutten: ikke grønt lys.

Tiden går til det som kan svi

Renames, formattering og importer hoppes over. Igjen står det jeg ville blitt flau over å ikke forstå: type-resonnement, subtil semantikk, tradeoffs. Og særlig alt som ble foreslått av AI-en, eller som «bare funker».

Det finner svakheter i koden også

Når jeg påstår noe, kjører Claude en sjekk for å se om det stemmer — og noen ganger må vi skrive om koden etterpå. Når du har brukt et par timer på å prompte fram en løsning, er det fort gjort å se raskt over den og sende den videre.

Et ekte intervju: den forsvunne pluginen

Vi oppdaterte noen dependencies for å tilfredsstille Dependabot. Underveis fjernet Claude io.spring.dependency-management fra build.gradle.kts. Uten intervjuet hadde jeg tenkt «ja ja, da trenger vi vel ikke den» og gått videre. Slik gikk det i stedet:

claude — pre-review-intervju
C
Én ting igjen før denne forlater draft. Diffen fjerner id("io.spring.dependency-management") fra build.gradle.kts. Hvorfor trenger vi den ikke lenger?
Det vet jeg ikke...
K
C
Helt greit svar — det er derfor vi gjør dette. Vi tar det nedenfra. Ikke om pluginen ennå: hva er egentlig testImplementation i Gradle?
Det er ikke et byggesteg. Det er en configuration, altså en bøtte. Jeg legger koordinater i den som tekststrenger, "org.eksempel:bibliotek:1.4.2". Ingenting er løst opp på det tidspunktet.
K
C
Bra. Så bøtta fylles i konfigurasjonsfasen. Da: to biblioteker i grafen drar inn samme modul i to ulike versjoner. Hvem vinner, og når avgjøres det?
Det avgjøres i resolusjonsfasen, når noen faktisk ber om classpathen. Gradle holder en avstemning over hele grafen, og default er at høyeste versjon vinner. Et BOM lagt inn med platform(...) bidrar med constraints; det er stemmer, ikke ordre.
K
C
Da har du svaret ditt liggende. Dependabot ba oss om 2.9.1. BOM-en sier 2.7.4. Etter reglene du nettopp beskrev — hvem vinner?
Min. 2.9.1 er høyest.
...men det var jo nettopp det som ikke skjedde. Derfor satt vi og lurte i går.
K
C
Der er sprekken. Så hypotesen din må være at io.spring.dependency-management ikke deltar i den avstemningen. Hva gjør den i stedet?
Den emulerer Maven. Maven har ikke avstemning; dependencyManagement er et dekret. Så pluginen henger seg på resolusjonen og setter versjonen på hver modul den styrer. Den stemmer ikke. Den overskriver.
K
C
Presiser «henger seg på». Kunne jeg ikke bare flyttet min egen deklarasjon lenger ned i fila, så min kom sist?
Nei, og det er hele poenget. Rekkefølge i build-fila er irrelevant. Hele scriptet kjøres ferdig i konfigurasjonsfasen, og alt det gjør er å fylle bøtter. Pluginens regel kjører i resolusjonsfasen, etter at hele scriptet er lest og etter at avstemningen har konkludert. Det er ikke en avstemning jeg kan vinne ved å rope sist. Det er en overskriving som skjer etterpå.
K
C

Det sitter. La oss ikke tro på det — Gradle sier det selv. Jeg kjører den med og uten pluginen:

$ ./gradlew dependencyInsight \
    --configuration runtimeClasspath --dependency bibliotek
# før (med pluginen)
org.eksempel:bibliotek:2.7.4
Selected by rule
# etter (uten pluginen, BOM som platform)
org.eksempel:bibliotek:2.9.1
By conflict resolution: between 2.7.4 and 2.9.1

Selected by rule mot By conflict resolution — det er nøyaktig skillet du beskrev, med Gradles egne ord. Siste spørsmål: hva mister vi ved å fjerne den?

Ingenting vi trenger. Spring Boot-pluginen har brukt Gradles egen platform-støtte siden 2.x — BOM-en kommer inn som implementation(platform(SpringBootPlugin.BOM_COORDINATES)). Vi beholder de samlede versjonene. Det vi mister er Maven-semantikken der BOM-en trumfer meg, og det var jo den som blokkerte sikkerhetsoppdateringen.
K
C

Dette sitter. Du kan trygt gå til review.

Det du nå eier, og som bør stå i PR-beskrivelsen: configurations er bøtter med strenger; versjoner avgjøres ved avstemning i resolusjonsfasen; et BOM som platform stemmer, mens io.spring.dependency-management overskriver etterpå — og det er derfor overstyringen din ikke slo gjennom.

Det er dette som er forskjellen på en grønn test og en forstått endring. Testene var grønne hele veien. Det var forklaringen som manglet — og den forklaringen er det eneste som gjør at neste person som ser rart på build.gradle.kts ikke legger pluginen inn igjen.

Kortere runde: hvorfor inline her?

Ikke alle intervjuer er lange. Dette tok fire minutter, og det står igjen fordi det første svaret mitt var riktig og likevel ikke svaret.

claude — pre-review-intervju
claude withLock er merket inline. Hvorfor?
knut Ytelse. Vi slipper å allokere et lambda-objekt per kall, og JVM-en slipper et virtuelt kall gjennom Function0.invoke.
claude

Begge deler er sant, og ingen av dem er grunnen. Se på kallstedet ditt igjen:

fun findUser(id: UserId): User? {
    withLock {
        cache[id]?.let { return it }
    }
    return loadFromDb(id)
}

Hva returnerer den linja fra?

knut ...fra findUser. Ikke fra lambdaen. Den hopper ut av hele funksjonen, forbi loadFromDb.
claude

Det er et non-local return, og det er hele grunnen. Fjern inline og bygg:

$ kotlinc findUser.kt
error: 'return' is prohibited here.
cache[id]?.let { return it }
knut Ja, bare inline-funksjoner tillater det. Koden ekspanderes rett inn i findUser som om den var skrevet der, så return er et ekte return derfra. Uten inline er lambdaen et eget objekt med sin egen invoke, og da finnes det ingenting å returnere fra. Ytelsen er en bieffekt. Det jeg faktisk kjøpte var lov til å hoppe ut.
claude Da tar vi den andre parameteren. Hvorfor står det crossinline på den?
knut

Fordi den ikke kalles direkte i bodyen til launchTraced. Den havner inne i launch { }:

inline fun CoroutineScope.launchTraced(
    name: String,
    crossinline block: suspend () -> Unit
): Job = launch { block() }

Et non-local return derfra skulle returnert fra launchTraced, som for lengst har returnert en Job. Stackframen finnes ikke lenger. Så jeg lover at lambdaen ikke gjør det.

claude

Fjern den og se hva du får. Det er to feil, ikke én:

error: cannot inline 'block: suspend () -> Unit' here:
it might contain non-local returns. Add 'crossinline'
modifier to parameter declaration 'block'.
error: suspend inline lambda parameters of non-suspend
function type are not supported. Add 'noinline' or
'crossinline' modifier.

Den andre er ikke en konsekvens av den første. Hva sier den?

knut At en suspend-lambda i en ikke-suspend inline-funksjon ikke kan inlines i det hele tatt, uansett hvor jeg kaller den. Suspend-bodyen kompileres til en tilstandsmaskin med sin egen klasse; den kan ikke limes inn i en vanlig funksjon. Derfor dukker crossinline opp så ofte rundt coroutines: det er ikke et coroutine-triks, det er den eneste veien inn.
claude Siste: hvem stoppet deg?
knut

Kompilatoren. Ikke runtime. Det er ingen sjekk som feiler i produksjon en torsdag; det er kode som aldri blir bygget. crossinline er en kontrakt jeg signerer på kompileringstidspunktet, ikke en vakt som står der og passer på.

Og kompilatoren sier rett ut hva jeg skal skrive. Jeg hadde lest den meldingen et titalls ganger uten å lese den.

Alle fire kompilatorutskriftene over er kjørt, ikke husket — kotlinc 2.1.0. Det er samme regel som gjelder i intervjuet: kan en påstand sjekkes, så sjekkes den.

Tonen

Et intervju som føles som en eksamen blir besvart som en eksamen — med gjetting.

Samarbeidende, ikke eksaminerende
Dette beskytter utvikleren, det tar hen ikke.
Ærlig framfor snill
Poenget er å avdekke hull, ikke å bli fort ferdig. Et «nesten riktig» svar er nettopp det vi prøver å fange.
Forankret
Pek på faktiske symboler, filer og linjer i diffen. Og verifiser lokalt framfor å påstå — kjør kompilatoren, ikke hukommelsen.

Skillen

Hele greia er én markdown-fil. Legg den i ~/.claude/skills/pre-review-intervju/SKILL.md, si «klar for review», og la være å svare for fort.

Last ned SKILL.md
SKILL.md
---
name: pre-review-intervju
description: Intervju utvikleren om endringene FØR de går til review, så hen forstår alt (hvordan og hvorfor) før noen andre ser det. Bruk når utvikleren er klar for review, skal opprette/oppdatere en PR, ber om å bli intervjuet, eller sier "før review"/"klar for review". Gate mot å dele noe hen ikke fullt ut eier.
---

# Pre-review intervju

Formål: Ingen skal gi fra seg en endring til review som de ikke selv forstår
fullt ut, både *hvordan* den virker og *hvorfor* den ble gjort slik. Denne skill-en
kjøres før vi går til review. Du (Claude) intervjuer utvikleren til du er trygg på
at forståelsen sitter. Dette er en sperre: ikke gi grønt lys før den er passert.

## Når den kjører

Foretrukket rekkefølge: opprett draft-PR først, kjør så dette intervjuet før PR-en
tas ut av draft / sendes til reell review. En draft er ikke "delt med andre" i den
forstand denne skill-en vokter mot — reviewere varsles først når den forlater draft.

- Standard: rett etter at draft-PR-en er opprettet/oppdatert, før den forlater draft.
- Når utvikleren sier hen er klar for review, eller ber om intervju.
- Etter at en endring er ferdig, men før den deles med andre (dvs. før draft fjernes).

Er utvikleren i ferd med å ta en PR ut av draft eller be om reell review uten å ha
kjørt denne, minn om den først.

## Slik gjør du det

1. **Kartlegg endringene.** Hent diffen (working tree, branch vs base, eller PR).
   Del den i logiske enheter, én idé per bolk, ikke fil for fil.

2. **Prioriter det som kan svi.** Hopp over det trivielle (rename, formattering,
   importer). Bruk tiden på det utvikleren ville blitt flau over å ikke forstå:
   type-resonnement, subtil semantikk, tradeoffs, hvorfor denne tilnærmingen og
   ikke en annen, hva som ville skjedd ved alternativer, og særlig alt som ble
   foreslått av Claude/AI eller som "bare funker".

3. **Intervju, ikke foredrag.** Still ett fokusert spørsmål om gangen og vent på
   svar i utvikleren sine egne ord. Ikke server svaret i spørsmålet. Veksle mellom:
   - *Hvordan:* "Hva gjør denne linja konkret?" "Hvorfor kompilerer dette?"
   - *Hvorfor:* "Hvorfor denne løsningen og ikke X?" "Hvilket problem løser den?"
   - *Press som en reviewer ville:* "Hva skjer hvis ...?" "Hvorfor er dette trygt?"
     "Hva er kostnaden?"

4. **Ikke godta håndvifting.** Et "nesten riktig" eller vagt svar er ikke godt nok
   her, for det er nettopp det vi prøver å fange. Si rett ut at det ikke sitter,
   forklar konkret, gjerne med en analogi og en repetisjon, og still spørsmålet på
   nytt til det sitter. Verifiser lokalt hvis et svar kan sjekkes (kompiler, kjør),
   heller enn å påstå.

5. **Avslutt med en forståelses-sjekk.** Oppsummer kort hva utvikleren har bekreftet
   at hen forstår, og flagg ærlig det som fortsatt vakler. Vakler noe: ikke grønt lys.

## Tone

- Samarbeidende, ikke eksaminerende. Dette beskytter utvikleren, det tar hen ikke.
- Ærlig framfor snill: poenget er å avdekke hull, ikke å bli fort ferdig.
- Forankret: pek på faktiske symboler, filer og linjer i diffen.

## Resultat

Når alt sitter: si det rett ut ("Dette sitter, du kan trygt gå til review") og
oppsummer de viktigste poengene utvikleren nå eier, så de kan gjenbrukes i
PR-beskrivelse eller på Slack.