Kompleksitas Siklomatik: Gerbang CI dan Risiko Canary
Summary
Kompleksitas siklomatik adalah ukuran jalur independen dalam fungsi. Ambang batas CI yang umum (10 per fungsi) tidak menangkap risiko sebenarnya—delta kompleksitas dalam diff yang dikirim adalah sinyal yang lebih kuat untuk incident. Gating canary pada complexity delta, cross-checked dengan test delta, lebih efektif daripada metrik repo-wide.
Kompleksitas siklomatik menghitung jumlah jalur independen melalui fungsi. NIST menetapkan ceiling yang aman di 10 per fungsi, dan sebagian besar tim menerapkan sesuatu yang mirip dengan itu di CI. Angka tersebut tidak memberitahu Anda jalur mana yang sebenarnya akan dieksekusi oleh canary yang Anda akan kirim ke 5% traffic produksi, dan kesenjangan itulah tempat sebagian besar insiden rollout sebenarnya dimulai. Perlakukan kompleksitas siklomatik sebagai input risiko rollout, bukan rule lint, dan angka mulai memberikan nilai yang layak. Dalam artikel ini, kami akan mengeksplorasi mengapa ambang batas statis tidak cukup, bagaimana delta kompleksitas mengungkapkan testing burden sebenarnya, dan cara mengintegrasikan metrik ini ke dalam pipeline canary Anda sebelum incident terjadi.
Apa yang Sebenarnya Diukur Kompleksitas Siklomatik
Metrik McCabe adalah penghitungan graph: nodes, edges, connected components. Setiap if, for, while, case, dan boolean operator menambah satu jalur. Fungsi dengan kompleksitas 25 memiliki setidaknya 25 jalur yang linearly independent melaluinya. Itu bukan pendapat, itu basis path testing math, dan itu menetapkan floor untuk berapa banyak test case yang Anda butuhkan untuk cover fungsi sekali, end to end. Breakdown Sourcegraph memiliki threshold lengkap: 1-10 low risk, 11-20 moderate, 21-50 high, 50+ adalah tier "kita harus bicara."
Sebagian besar tim tidak mencapai floor itu. Coverage tools melaporkan line coverage, bukan path coverage, jadi fungsi dapat menunjukkan 90% coverage sementara separuh branch-nya tidak pernah fire di CI. Itu adalah bagian yang tidak ada di PR template, dan itu alasan "tests pass" dan "ini aman untuk ship" adalah dua klaim yang berbeda tetapi diperlakukan sebagai satu.
Kami telah meninjau cukup post-mortem untuk menyadari pola: incident review selalu memiliki baris untuk error budget burn dan baris untuk time to detect. Hampir tidak pernah ada baris untuk seperti apa kompleksitas fungsi yang diubah sebelum diff landed. Itu adalah kesenjangan dalam paperwork, bukan hanya tooling.

Mengapa Angka Kompleksitas Flat Menyembunyikan Blast Radius Sebenarnya
Inilah bagian yang salah dipahami setiap complexity dashboard: dua fungsi dapat memposting score kompleksitas siklomatik yang identik dan membawa risiko yang sangat berbeda. Switch statement 20-branch yang mendispatch ke handler yang well-tested bukan hewan yang sama dengan fungsi yang memiliki empat pocket terpisah dari conditional nested yang tersebar di 80 baris, masing-masing tiga level deep. CodeScene menyebut bentuk kedua "bumpy road," dan namanya akurat. Itu kedalaman nesting, bukan jumlah branch mentah, yang mengganggu working memory dan menyembunyikan edge case yang tidak ada yang pikirkan untuk test. Writeup CodeScene memandu perbandingan secara detail, dan itu map dengan bersih ke apa yang kami lihat dalam data rollout.
Ini penting bagi kami secara khusus karena canary tidak gagal pada average complexity. Itu gagal pada fungsi bumpy satu itu yang di-touch dalam diff ini, di 2am, under load yang tidak ada yang load-test. Dalam post-mortem yang kami tinjau dengan platform teams, pola berulang: fungsi root-cause hampir tidak pernah memiliki score kompleksitas tertinggi di repo. Itu memiliki complexity delta tertinggi dalam diff yang dikirim. Itu sinyal yang berbeda, dan hampir tidak ada yang instrument-nya.
Dashboard yang memberitahu Anda complexity codebase trending down bukan dashboard yang sama yang memberitahu Anda rollout ini, right now, menyentuh fungsi yang baru saja mengambil tiga branch nested baru. Satu adalah health report yang Anda baca sekali quarter. Yang lainnya adalah gate yang Anda benar-benar ingin di pipeline deploy, duduk di sebelah SLO check, bukan buried di separate static-analysis tool yang tidak ada yang buka selama incident.
Ring-based dan percentage-based rollout sudah assume beberapa diff lebih risky daripada yang lain; itu adalah entire premise dari canary. Yang sebagian besar pipeline tidak lakukan adalah let complexity shape dari diff itself inform berapa fast canary itu seharusnya widen. Itu diperlakukan sebagai code-review concern, kemudian forgotten saat PR merge.
CI Gate yang Semua Orang Set di 10, dan Mengapa Itu Tidak Menggerakkan Jarum
Nasihah standar, cap cyclomatic complexity di 10 dan fail build di atas itu, adalah "skip" step yang hampir setiap engineering blog merekomendasikan dan hampir tidak ada yang validate terhadap actual incident data. Itu tidak salah, exactly. Itu incomplete dengan cara yang let teams percaya mereka telah handle risiko ketika mereka sebagian besar hanya pindahkan-nya.
Dua failure modes, both common di tim yang kami bicara:
Gaming the number. Extract Method adalah textbook fix, dan itu bekerja: cyclomatic complexity per fungsi drop. Tetapi jika extraction tidak reduce actual decision count, hanya relocate itu across tiga fungsi instead of one, system complexity tidak berubah. Gate go green. Blast radius tidak shrink, itu just gets harder to see dalam single diff view.
Ignoring the shape. Flat threshold treat 12-branch dispatcher dan 12-path bumpy road sebagai equally risky. Mereka tidak. Dispatcher probably fine; itu mechanical routing. Bumpy road adalah di mana your next rollback lives, karena orang yang review itu stopped tracking state tiga level nested in.
AI coding agents membuat ini worse sebelum itu make it better. Devin dan autonomous agent serupa ship PR fast, dan "fast" sering berarti adding branch instead of refactor yang sudah di sana. Itu path of least resistance untuk model optimizing untuk passing test suite, bukan untuk reviewer's cognitive load. Jika team Anda merge AI-authored diff pada volume, complexity delta per PR adalah metric yang Anda ingin di dashboard sebelum itu menjadi incident review finding, bukan after.

Apa yang Complexity Delta Benar-benar Katakan Tentang Testing Burden
Lupakan absolute score sebentar. Angka yang memprediksi incident risk adalah change dalam complexity yang diintroduksi oleh single diff, cross-referenced terhadap apakah tests touching diff itu benar-benar grown untuk match-nya.
Fungsi yang go dari complexity 8 ke 19 dalam one PR has, by basis-path-testing math di atas, roughly doubled minimum required test count-nya. Jika PR add two tests, Anda telah ship coverage gap disguised sebagai passing CI run. Gap itu tidak show hingga canary hit 5% traffic slice yang exercise untested branch, dan by then itu incident, bukan code review comment sitting unresolved di PR thread.
Ini adalah instrumentation gap yang upstreamapi's AI Pilot built untuk close di rollout side: gate canary percentage dan hold time di complexity delta dari shipped diff, cross-checked terhadap test delta-nya, bukan just di downstream error-rate SLO. Error-rate SLO memberitahu Anda something sudah break. Complexity-delta gate memberitahu Anda diff lebih likely untuk break something sebelum itu serving live traffic, yang merupakan satu-satunya point di mana info itu still actionable.
// Simplified canary gate: widen slowly pada low-risk diff, hold pada high-risk
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 };
}Apakah Research Benar-benar Setuju dengan Ini?
Tidak, dan itu worth saying plainly. Beberapa practitioner research push back hard pada cyclomatic complexity sebagai defect predictor sama sekali, arguing teams yang optimize untuk lower score sering just relocate complexity di somewhere kurang visible. Kritik GetDX makes case ini dan points tim toward developer-experience metrics instead. Kami tidak pikir argumen itu kill metric; kami pikir itu kill metric used alone, sebagai static repo-wide score, disconnected dari diff dan rollout itu attached ke.
Cara Kami Benar-benar Gating Canary pada Complexity, Bukan Hanya Error Rate
Tiga hal, dalam order berapa banyak friction mereka add ke PR:
Compute complexity delta per diff, bukan per repo. Repo-wide averages hide satu fungsi yang matter minggu ini. Delta per PR cheap untuk compute di most static analysis tools, dan itu number yang correlates dengan apa benar-benar break di following 48 jam.
Cross-reference terhadap test delta, bukan test count. Fungsi dengan 40 tests dan complexity jump dari 8 ke 19 dengan zero new tests adalah bigger risk daripada fresh function dengan complexity 15 dan matching coverage dari day one.
Feed ratio itu ke rollout pacing, bukan just merge approval. High complexity-delta diff tidak perlu di-block di review; plenty dari legitimately complex domain logic (state machines, protocol parsers) akan selalu score high. Itu butuh slower canary dan shorter error-budget leash, yang merupakan rollout decision, bukan code review decision.
Write runbook entry sebelum rollout, bukan setelah page. Jika on-call engineer membuka incident channel di 3am has untuk reverse-engineer mengapa "clean" PR baru saja burn error budget, documentation failed sebelum code did. One-line note di deploy log ("complexity delta 11, test delta 1, held di 20%") biaya nothing untuk write dan save first fifteen menit dari setiap incident review yang follows.

Apakah Itu Worth Tracking, atau Hanya Angka Dashboard Lainnya?
Depends entirely pada di mana Anda attach-nya. Cyclomatic complexity sebagai repo-health trend line kebanyakan decoration: nice untuk quarterly slide, useless di 2am. Cyclomatic complexity sebagai per-diff delta, fed ke canary pacing dan cross-checked terhadap test growth, adalah salah satu dari cheaper leading indicators yang kami telah find untuk "rollout ini akan page someone."
Post-mortem akan tanya apa SLO budget seperti sebelum rollback. Increasingly, milik kami juga tanya apa complexity delta terlihat seperti di diff yang shipped. Worth adding pertanyaan itu ke runbook Anda sendiri sebelum incident force conversation, bukan during retro ketika jawaban adalah shrug dan promise untuk "add better tests next time."