Cyklomatisk komplexitet: från review till deploymentgat
Summary
Cyklomatisk komplexitet räknar antalet vägar genom kod och sätter ett golv för testbetsbörda. En statisk tröskel på 10 fungerar inte väl – två funktioner med samma komplexitet kan bära helt olika deploymentisk. Det tal du faktiskt behöver är delta per diff korsreferenserad mot testökning för att minska risk. Gata canary på detta förhållande istället för statiska siffror för att fånga problem innan produktionen.
Cyklomatisk komplexitet räknar antalet oberoende vägar genom en funktion. NIST rekommenderar en maxgräns på 10 per funktion, och de flesta team tvingar fram något liknande i CI. Det som den siffran inte säger är vilken av dessa vägar som faktiskt kommer att köras av canary du är på väg att skicka till 5% av produktionstrafiken, och det gapet är där de flesta deploymentincidenter faktiskt startar. Behandla cyklomatisk komplexitet som en deploymentriskinmatning, inte en lintingregel, och siffran börjar tjäna sitt syfte. Det här är en blind fläck som väl värd att adressera före du skickar nästa release.
Vad cyklomatisk komplexitet faktiskt mäter
McCabes metrik är en grafökning: noder, kanter, sammanhängande komponenter. Varje if, for, while, case och boolesk operator lägger till en väg. En funktion med komplexitet 25 har minst 25 linjärt oberoende vägar genom sig. Det är inte åsikt, det är grundvägstestningsmatematik, och det sätter ett golv för hur många testfall du skulle behöva för att täcka funktionen en gång, från början till slut. Sourcegraphs sammanfattning innehåller alla trösklarna: 1-10 låg risk, 11-20 måttlig, 21-50 högt, 50+ är nivån "vi borde prata om det".
De flesta team når inte det golvet. Täckningsverktyg rapporterar radtäckning, inte vägtäckning, så en funktion kan visa 90% täckning medan hälften av dess grenar aldrig körs i CI. Det är den delen som ingen skriver på PR-mallen, och det är anledningen till att "tester passerar" och "det är säkert att skicka" är två olika påstenden som behandlas som ett.
Vi har granskat tillräckligt många postmortemrapporter för att lägga märke till mönstret: incidentrapporten har alltid en rad för felbudgetförbrukning och en rad för tid för detektering. Den har nästan aldrig en rad för hur komplexiteten såg ut på den ändrade funktionen innan diffen landade. Det är ett gap i pappersarbetet, inte bara verktygen.

Varför en platt komplexitetssiffra döljer den verkliga explosionsradien
Här är delen som varje komplexitetsdashboard får fel: två funktioner kan visa samma cyklomatiska komplexitetssiffra och bära helt olika risk. En 20-grens switchsats som dirigerar till vältestade hanterare är inte samma djur som en funktion med fyra separata fickor av kapslade villkor spridda över 80 rader, var och en tre nivåer djup. CodeScene kallar den andra formen en "bumpy road", och namnet är korrekt. Det är kapslingsjupet, inte det råa grennumret, som belastar arbetsminnet och döljer kantfallet som ingen tänkte på att testa. CodeScenes artikel går igenom jämförelsen i detalj, och det mappar rakt in i vad vi ser i deploymentdata.
Det här spelar en roll för oss specifikt eftersom en canary inte misslyckas på genomsnittlig komplexitet. Den misslyckas på den ena bumpy-funktionen som rörktes i denna diff, vid 02:00, under belastning som ingen lastat. I postmortemrapporterna vi granskat med platformteam upprepas mönstret: root-cause-funktionen har nästan aldrig den högsta komplexitetssiffran i repot. Den har den högsta komplexitetsdelta i diffen som shippades. Det är en annan signal, och nästan ingen instrumenterar den.
En dashboard som säger dig att kodbasen komplexitet trendar ned är inte samma dashboard som säger dig att denna deployment, just nu, rörde en funktion som just tog upp tre nya kapslade grenar. Den ena är en hälsorapport du läser en gång per kvartal. Den andra är ett gat du faktiskt skulle vilja ha i deploymentpipelinen, sittande bredvid SLO-checken, inte gräven i ett separat statiskt analysverktyg som ingen öppnar under en incident.
Ringbaserade och procentbaserade rollouts antar redan att vissa diffs är risker än andra; det är hela premissen för en canary. Det de flesta pipelines inte gör är att låta komplexitetsformen i själva diffen informera hur snabbt canary bör vidgas. Det behandlas som en kodgranskningsfråga, sedan glömd när PR:n mergas.
CI-gatet alla sätter på 10 – och varför det inte förändrar något
Standardrådet, cap cyklomatisk komplexitet på 10 och failera builden över det, är "skips"-steget nästan varje engineeringblogg rekommenderar och nästan ingen validerar mot faktiska incidentdata. Det är inte fel, exakt. Det är ofullständigt på ett sätt som låter team tro de hanterat risken när de mestadels bara flyttat den.
Två misslyckandesmoder, båda vanliga på team vi talat med:
Spela siffran. Extract Method är lärobok-fixet, och det fungerar: cyklomatisk komplexitet per funktion sjunker. Men om extraheringen inte minskar det faktiska beslutsantalet, bara förflyttar det över tre funktioner istället för en, är systemets komplexitet oförändrad. Gatet blir grönt. Explosionsradien krymper inte, den blir bara svårare att se i en enda diffvy.
Ignorera formen. En platt tröskel behandlar en 12-grens dispatcher och en 12-vägs bumpy road som lika riskabelt. De är inte. Dispatchern är förmodligen fin; den är mekanisk routing. Den bumpy roaden är där din nästa rollback bor, för personen som granskar det slutade spåra tillstånd tre kapslingsnivåer in.
AI-kodningsagenter gör detta värre innan det blir bättre. Devin och liknande autonoma agenter shippades PR:er snabbt, och "snabbt" betyder ofta att lägga till en gren istället för att refaktorera den som redan är där. Det är vägen med minst motstånd för en modell optimerad för ett passerande testsvit, inte för en granskarens kognitiva belastning. Om ditt team mergar AI-författade diffs i volym, är komplexitets-delta per PR en metrik du vill ha på dashboarden innan det blir ett incidentreviefynd, inte efter.

Vad komplexitets-delta faktiskt säger dig om testbörda
Glöm den absoluta poängen ett tag. Det tal som förutspår incidentrisk är förändringen i komplexitet som införs av en enda diff, korsreferenserad mot huruvida testerna som rör denna diff faktiskt växte för att matcha den.
En funktion som går från komplexitet 8 till komplexitet 19 i ett PR har, enligt grundväg-testningsmatematiken ovan, ungefär fördubbla sitt minimikrav på testantal. Om PR:n lade till två tester, har du shippat ett täckningsgap döljt som en passerande CI-körning. Det gapet dyker inte upp förrän canary träffar den 5% trafikslisen som utövar den otestade grenen, och vid dess är det en incident, inte en kodgranskningskommentar som sitter olöst i en PR-tråd.
Det här är instrumenteringsgapet upstreamapis AI Pilot är byggt för att stänga på deploymentssidan: gat canary-procenten och håll tid på komplexitets-delta i den shippade difven, korsreferenserad mot dess testdelta, inte bara på det nedströmska fel-rate SLO:t. Ett error-rate SLO säger dig något redan bröt. Ett komplexitets-delta-gat säger dig difven var mer sannolikt att bryta något innan den serverar livetrafik, vilket är den enda punkten där denna information fortfarande är handlingsbar.
// Förenklad canary-gat: vidga långsamt på lågriskdiffs, håll på högriskiga
function canaryStep(diff: DiffMetrics): CanaryDecision {
const complexityRisk = diff.complexityDelta / Math.max(diff.testDelta, 1);
if (complexityRisk > 3 && diff.slo.errorBudgetBurn > 0.1) {
return { action: "hold", trafficPct: diff.currentTrafficPct };
}
if (complexityRisk > 3) {
return { action: "extend_bake_time", bakeMinutes: 45 };
}
return { action: "advance", trafficPct: diff.currentTrafficPct + 10 };
}Håller forskningen redan med om detta?
Nej, och det är värt att säga klart. Viss praktikerforskning skjuter tillbaka hårt mot cyklomatisk komplexitet som en felfelsföreseende överhuvudtaget, hävdar att team som optimerar för en lägre poäng ofta bara förflyttar komplexitet någonstans mindre synligt. GetDX:s kritik gör det här fallet och pekar team mot utvecklarupplevelsemätningar istället. Vi tror inte det argumentet dödar mätaren; vi tror det dödar mätaren som används ensam, som en statisk repo-bred poäng, frånkopplad från difven och deployments det är kopplat till.
Hur vi faktiskt gåtar en canary på komplexitet, inte bara felfrekvens
Tre saker, i ordning hur mycket friktion de lägger till ett PR:
Beräkna komplexitets-delta per diff, inte per repo. Repo-breda genomsnitt döljer den ena funktionen som spelar roll denna vecka. Delta per PR är billig att beräkna i de flesta statiska analysverktyg, och det är antalet som korrelerar med vad som faktiskt bryter i de följande 48 timmarna.
Korsreferenser mot testdelta, inte testantal. En funktion med 40 tester och en komplexitetssprång från 8 till 19 utan nya tester är en större risk än en ny funktion med komplexitet 15 och matchande täckning från dag ett.
Mata förhållandet in i deploymentshastighet, inte bara merge-godkännande. En högt komplexitets-delta diff behöver inte blockeras vid granskning; massor av legitim komplex domänlogik (tillståndsmaskiner, protokollanalysare) kommer alltid att få hög poäng. Det behöver en långsammare canary och en kortare felbudgetsnara, vilket är ett deploymentsbeslut, inte ett kodgranskningsbeslut.
Skriv runbookingången innan deployments, inte efter sidan. Om on-call-ingenjören som öppnar incidentkanalen vid 03:00 måste reversering varför en "ren" PR just brände felbudgeten, misslyckades dokumentationen före koden. En enradsanmärkning i deploymentloggen ("komplexitets-delta 11, testdelta 1, höll på 20%") kostar inget att skriva och sparar de första femton minuterna av varje incidentgranskning som följer.

Är det värt att spåra, eller bara ett annat dashboardnummer?
Beror helt på var du fäster det. Cyklomatisk komplexitet som en repo-hälsotrend är mestadels dekoration: fin för en kvartalsslide, värdelös vid 02:00. Cyklomatisk komplexitet som en per-diff-delta, matad in i canary-takten och korsreferenserad mot testökning, är en av de billigare ledande indikatorerna vi hittat för "denna deployment kommer att väcka någon."
Postmortemrapporten kommer att fråga hur SLO-budgeten såg ut innan rollbacken. I ökad omfattning frågar våra också hur komplexitets-delta såg ut på difven som shippades. Det är värt att lägga denna fråga till din egen runbook innan incidenten tvingar konversationen, inte under retrot när svaret är ett axelzuckande och ett löfte om "bättre tester nästa gång."