Vibe Coderin El Kitabı · Bölüm 5.1 / 13

Vibe Coder #5.1: Teknik Borç ve Spagetti Kod – AI'ın Faizli Kredisi

AI ile kod üretmek ucuzladı, kodu birleştirmek ucuzlamadı. Teknik borcun faizi takvime değil, karmaşık dosyaya her dokunuşunuza işliyor; düz PHP ile yazılmış bir borç ölçer ve dört ajanda yeni borcu durduran bir kanca.

13 dk okuma Vibe Coding

Bu yazının İngilizcesi de var →
Ahşap masada spagetti dolu tabağın kenarına bırakılmış temassız kredi kartı, tabağın altından masadan sarkan uzun bir hesap ekstresi ve yanında bakır bozuk paralar: teknik borcun faizli kredi metaforu.

Serinin ilk bölümünde bir senet imzalamıştık: "Bölüm 5.1'de detaylandıracağımız üzere, sentaksı unutmak serbesttir ancak mimariyi unutmak projenin sonunu hazırlar." Yedi ay sonra vade geldi. Bir önceki bölüm de faturayı önümüze koyarak kapandı: dilimleri o kadar küçültürsünüz ki her biri kendi içinde doğru, toplamı yanlış olur. On küçük commit, hiçbiri kötü değil, ama mimari kimsenin karar vermediği bir yere kaymış. Bu bölüm o kaymanın adını koyuyor: teknik borç (technical debt).

Metafor kredi kartı. AI kodlama ajanı elinize limitsiz, temassız bir kart verdi; harcamak hiç bu kadar kolay olmamıştı. Ama ekstre aynı ekstre. Borcun nereden geldiğine, faizin nerede işlediğine ve ajana "kart limiti" koymanın dört araçtaki karşılığına bakıyoruz. Sonunda elinizde depoya karşı çalışan bir borç ölçer olacak.

Teknik borç nedir, ilk kim yazdı?#

Terimin doğum belgesi belli. Ward Cunningham, 1992'de OOPSLA'da sunduğu WyCash deneyim raporunda şöyle diyor: "İlk sürüm kodu yayına almak borçlanmak gibidir." Hemen ardından da faizi tanımlıyor: tam doğru olmayan kod üzerinde harcanan her dakika o borcun faizi sayılır.

Çoğu alıntıda atlanan kısım, Cunningham'ın borçlanmaya karşı olmaması. Aynı paragrafta erken kestirmenin işe yaradığını, yeter ki hızlı bir yeniden yazımla geri ödensin diyor. Tehlike borcun kendisi değil, ödenmemesi. Yeterince birikirse koca bir mühendislik ekibini yerinde saydırabiliyor.

Martin Fowler 2009'da buna bir dörtlü tablo ekledi: borç bilinçli ya da kazara, ihtiyatlı ya da pervasız olabilir. Fowler'a göre işe yarayan ayrım borç var mı yok mu değil, ihtiyatlı borç ile pervasız borç arasında.

Acı Gerçek: AI ile çalışırken en çok dolan kadran "pervasız ve kazara" olanı. Bilinçli borçta en azından biri "bunu sonra düzelteceğiz" der ve bir yere not düşer. Ajan ise "bitti" der. Kimse borçlandığını bilmediği için kimse not düşmez. Kartınızdan çekim yapılmış, ama bildirim gelmemiş.

AI kartın limitini nasıl yükseltti?#

Bu konuda elimizdeki en büyük veri seti GitClear'ın 2025 kod kalitesi raporu. Ocak 2020 ile Aralık 2024 arasında yazılmış 211 milyon değişen satıra bakıyorlar. İki rakam kredi kartı metaforuna birebir oturuyor:

  • Taşınan kod azaldı. Bir fonksiyonu ortak bir yere taşımak refactor'un, yani borç ödemenin izi. Değişen satırlar içinde taşınanların payı 2021'de %25'ten 2024'te %10'un altına inmiş.
  • Kopyalanan kod arttı. Kopyala-yapıştır satırlarının payı aynı dönemde %8,3'ten %12,3'e çıkmış. Rapora göre kopyalanan kod, kayıtlarda ilk kez taşınan kodu geçmiş.

Google'ın DORA ekibi de aynı yöne bakıyor. 2024 raporunun duyurusunda (Nathen Harvey ve Derek DeBellis, 22.10.2024) cümle şu: AI benimsemesi arttıkça teslimat hızında tahmini %1,5, teslimat istikrarında tahmini %7,2 düşüş eşlik etmiş.

Teknik Detay: iki çalışma da ilişki ölçüyor, sebep değil; GitClear bu eğilimi AI asistanlarının yaygınlaşmasıyla ilişkilendiriyor. Veriler de 2024'ün; 2026'nın ajanlarıyla tekrarlanmış hâlini henüz görmedik. Ama yön tutarlı: kod üretmek ucuzladı, kodu birleştirmek ucuzlamadı.

Spagetti kod da şekil değiştirdi. Eskiden spagetti iç içe geçmiş if-else bloklarıydı ve Spagetti 2.0 yazısında mimari ölçekteki hâlini konuşmuştuk. AI'ın spagettisi daha sinsi: yan yana duran, birbirine benzeyen ama tam aynı olmayan kopyalar. Ajan mevcut fonksiyonu bulup genişletmek yerine yenisini yazıyor, çünkü yenisini yazmak ona daha ucuz. Size değil.

Faiz nerede işliyor?#

Kod borcunun kredi kartından önemli bir farkı var: faiz takvime değil, karta her dokunuşunuza işliyor. Hiç açmadığınız, hiç değiştirmediğiniz karmaşık bir dosya size bir kuruş faiz ödetmez. Her hafta içine girip bir şey değiştirdiğiniz karmaşık dosya ise her girişte ödetir. Cunningham'ın "tam doğru olmayan kod üzerinde harcanan her dakika" dediği dakikalar orada harcanıyor.

Bu fikri sahaya indiren isim Adam Tornhill. Your Code as a Crime Scene kitabında (ikinci baskı Şubat 2024) sürüm kontrolü geçmişinden "sıcak noktalar" (hotspots) çıkarıp teknik borcu buna göre önceliklendirmeyi anlatıyor. Kaba formülü şöyle özetleyebiliriz:

  • Ana para: dosyanın karmaşıklığı ve içindeki kopyalar.
  • Faiz oranı: o dosyaya ne sıklıkla dokunulduğu, yani commit sayısı.
  • Ödenecek tutar: ikisinin çarpımı.

Bu çarpım, "hangi borcu önce ödeyeyim?" sorusunun cevabını değiştiriyor. En karmaşık dosya değil, en karmaşık ve en sık değişen dosya önce gelir.

Ekstreyi okumak: borç ölçer#

Bu ölçümü göze bırakmamak için küçük bir betik yazdım. Çerçeve yok, Composer bağımlılığı yok, düz PHP 8.3 ve git. Her PHP dosyası için fonksiyon başına karmaşıklığı (1 + karar noktası sayısı), son N gündeki commit sayısını ve başka yerde de geçen altı satırlık kopya pencerelerini sayıyor. İkinci bir modu daha var, birazdan ajana bağlayacağız.

PHP
<?php
​
declare(strict_types=1);
​
/**
 * Borç ölçer: her PHP dosyası için karmaşıklık (ana para), değişim sıklığı (faiz)
 * ve kopya blokları sayar; karmaşıklığı değişimle çarpıp sıcak noktaları sıralar.
 * Rapor: php borc-olcer.php [--gun=180] [--ilk=10]
 * Kanca: php borc-olcer.php --tavan=10  (borcu büyüten değişiklikte 2 ile çıkar)
 * Git deposunun kökünden çalıştırın.
 */
​
$secenek = getopt('', ['gun::', 'ilk::', 'tavan::']);
$gun = (int) ($secenek['gun'] ?? 180);
$ilk = (int) ($secenek['ilk'] ?? 10);
​
function git(string $komut): array
{
    exec('git ' . $komut . ' 2>/dev/null', $cikti, $kod);
    if ($kod !== 0) {
        fwrite(STDERR, "git $komut başarısız (kod $kod). Deponun kökünde misiniz?\n");
        exit(1);
    }
    return $cikti;
}
​
/** Fonksiyon başına karmaşıklık: 1 + karar noktası sayısı. */
function karmasiklik(string $kod): array
{
    $kararlar = [T_IF, T_ELSEIF, T_FOR, T_FOREACH, T_WHILE, T_CASE, T_CATCH,
        T_BOOLEAN_AND, T_BOOLEAN_OR, T_LOGICAL_AND, T_LOGICAL_OR, T_COALESCE];
    $sonuc = [];
    $ad = null;
    $derinlik = 0;
    $govdeDerinligi = null;
    $tokenlar = token_get_all($kod);
​
    foreach ($tokenlar as $i => $t) {
        if (is_array($t) && $t[0] === T_FUNCTION) {
            if ($govdeDerinligi !== null) {
                continue; // iç içe closure dıştaki fonksiyona sayılır
            }
            for ($j = $i + 1; isset($tokenlar[$j]); $j++) {
                if (is_array($tokenlar[$j]) && $tokenlar[$j][0] === T_STRING) {
                    $ad = $tokenlar[$j][1];
                    break;
                }
                if ($tokenlar[$j] === '(') {
                    $ad = '{closure}';
                    break;
                }
            }
            continue;
        }
        if ($t === '{' || (is_array($t) && in_array($t[0], [T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES], true))) {
            $derinlik++;
            if ($ad !== null && $govdeDerinligi === null) {
                $govdeDerinligi = $derinlik;
                $sonuc[$ad] = ($sonuc[$ad] ?? 0) + 1;
            }
            continue;
        }
        if ($t === ';' && $govdeDerinligi === null) {
            $ad = null; // gövdesiz (abstract/interface) metot
            continue;
        }
        if ($t === '}') {
            if ($govdeDerinligi !== null && $derinlik === $govdeDerinligi) {
                $ad = null;
                $govdeDerinligi = null;
            }
            $derinlik--;
            continue;
        }
        if ($govdeDerinligi !== null && ($t === '?' || (is_array($t) && in_array($t[0], $kararlar, true)))) {
            $sonuc[$ad]++;
        }
    }
    return $sonuc;
}
​
/** Yorumlar ve PHP dışı HTML atılmış satırlar: lisans başlığı kopya sayılmasın. */
function yalnizKod(string $yol): array
{
    $kod = '';
    foreach (token_get_all(file_get_contents($yol)) as $t) {
        if (is_array($t) && in_array($t[0], [T_COMMENT, T_DOC_COMMENT, T_INLINE_HTML], true)) {
            $kod .= str_repeat("\n", substr_count($t[1], "\n"));
            continue;
        }
        $kod .= is_array($t) ? $t[1] : $t;
    }
    return explode("\n", $kod);
}
​
// Kanca modu: eski borcu affeder, yenisini durdurur. Yalnızca değişen dosyalara
// bakar; tavanı aşan bir fonksiyon yeniyse ya da HEAD'dekinden karmaşıksa 2 döner.
if (isset($secenek['tavan'])) {
    $tavan = (int) $secenek['tavan'];
    $degisen = array_unique(array_merge(
        git("diff --name-only HEAD -- '*.php'"),
        git("ls-files --others --exclude-standard -- '*.php'")
    ));
    $ihlal = [];
    foreach ($degisen as $yol) {
        if (!is_file($yol)) {
            continue; // silinen dosya borç azaltır
        }
        exec('git show ' . escapeshellarg("HEAD:$yol") . ' 2>/dev/null', $eski, $kod);
        $once = $kod === 0 ? karmasiklik(implode("\n", $eski)) : [];
        unset($eski);
        foreach (karmasiklik(file_get_contents($yol)) as $ad => $deger) {
            if ($deger > $tavan && $deger > ($once[$ad] ?? 0)) {
                $ihlal[] = sprintf('%s %s(): %d (önce %s, tavan %d)', $yol, $ad, $deger, $once[$ad] ?? 'yok', $tavan);
            }
        }
    }
    if ($ihlal !== []) {
        fwrite(STDERR, "Borç tavanı aşıldı. Bu fonksiyonları bölmeden devam etme:\n  " . implode("\n  ", $ihlal) . "\n");
        exit(2);
    }
    exit(0);
}
​
$dosyalar = git("ls-files '*.php'");
if ($dosyalar === []) {
    fwrite(STDERR, "Depoda PHP dosyası yok.\n");
    exit(1);
}
​
// Faiz: son N günde dosyaya dokunan commit sayısı.
$degisim = array_count_values(array_filter(
    git("log --since='$gun days ago' --format= --name-only -- '*.php'")
));
​
// Kopya: yorumsuz, boşluğu atılmış, anlamlı 6 satırlık pencereler birden fazla yerde geçiyor mu?
$pencereler = [];
foreach ($dosyalar as $yol) {
    $temiz = array_values(array_filter(
        array_map(fn (string $s): string => preg_replace('/\s+/', '', $s), yalnizKod($yol)),
        fn (string $s): bool => strlen($s) > 3 && !in_array($s, ['{', '}', '});', '];'], true)
    ));
    for ($i = 0; $i + 6 <= count($temiz); $i++) {
        $pencereler[md5(implode("\n", array_slice($temiz, $i, 6)))][] = $yol;
    }
}
$kopya = [];
foreach ($pencereler as $yerler) {
    if (count($yerler) > 1) {
        foreach ($yerler as $yol) {
            $kopya[$yol] = ($kopya[$yol] ?? 0) + 1;
        }
    }
}
​
$rapor = [];
foreach ($dosyalar as $yol) {
    $fonksiyonlar = karmasiklik(file_get_contents($yol));
    $toplam = array_sum($fonksiyonlar);
    $rapor[$yol] = [
        'commit' => $degisim[$yol] ?? 0,
        'toplam' => $toplam,
        'enYuksek' => $fonksiyonlar === [] ? 0 : max($fonksiyonlar),
        'kopya' => $kopya[$yol] ?? 0,
        'skor' => ($degisim[$yol] ?? 0) * $toplam,
    ];
}
uasort($rapor, fn (array $a, array $b): int => [$b['skor'], $b['toplam']] <=> [$a['skor'], $a['toplam']]);
​
printf("Son %d gün, %d PHP dosyası\n", $gun, count($dosyalar));
printf("%-7s %-8s %-9s %-6s %s\n", 'commit', 'karmaş.', 'en yük.', 'kopya', 'dosya');
foreach (array_slice($rapor, 0, $ilk, true) as $yol => $r) {
    printf("%-7d %-8d %-9d %-6d %s\n", $r['commit'], $r['toplam'], $r['enYuksek'], $r['kopya'], $yol);
}
​
$faizsiz = count(array_filter($rapor, fn (array $r): bool => $r['commit'] === 0 && $r['toplam'] > 0));
printf("Karmaşık ama son %d günde dokunulmamış dosya: %d (borç var, faiz işlemiyor)\n", $gun, $faizsiz);
exit(0);

İki gerçek depoda çalıştırdım (25.10.2026, PHP 8.3.6 CLI).

Birincisi, Kentsel Dönüşüm'ün demo binası altinkaynak. On PHP dosyası. src/Altinkaynak.php toplam karmaşıklıkta 26 ile ilk sırada ve 21 kopya penceresi taşıyor; bunlar Kentsel Dönüşüm #1'de bulduğumuz üç kopya metodun izi. src/Response/Response.php içindeki tek bir fonksiyon, decorateResponse(), tek başına 14. Ama commit sütunu son 180 günde her satırda sıfır; deponun bütün geçmişi 06.09.2020'deki 20 commit'ten ibaret. Betiğin son satırı: "Karmaşık ama son 180 günde dokunulmamış dosya: 6 (borç var, faiz işlemiyor)."

Bu, Kentsel Dönüşüm serisinde neden önce okuttuğumuzu da açıklıyor. Borç altı yıldır orada ve kimseye faiz ödetmedi. Biz o dosyalara dokunmaya başladığımız gün faiz de başlıyor.

İkincisi, yaşayan bir kütüphane: PHPMailer (LGPL-2.1, son sürüm etiketi v7.1.1, 18.05.2026; ana bakımcısı Marcus Bointon). Son 365 günde 202 commit, 131 PHP dosyası.

Bash
$ php borc-olcer.php --gun=365 --ilk=3
Son 365 gün, 131 PHP dosyası
commit  karmaş. en yük.  kopya  dosya
31      718      38        98     src/PHPMailer.php
11      177      33        12     src/SMTP.php
7       34       6         0      src/POP3.php
Karmaşık ama son 365 günde dokunulmamış dosya: 12 (borç var, faiz işlemiyor)

src/PHPMailer.php 5.603 satır, 129 fonksiyon; en karmaşığı preSend() 38. src/SMTP.php içinde authenticate() 33. Gerçek: bu tablo PHPMailer hakkında bir kalite hükmü değil. Yıllardır yaşayan bir kütüphanenin merkez sınıfının en sık değişen ve en karmaşık dosya olması şaşırtıcı değil. Tablonun söylediği tek şey şu: bu depoda bir saat refactor zamanınız varsa, o saatin nereye gideceği belli. On iki karmaşık dosyaya ise bir yıldır kimse dokunmamış; onlar şimdilik bekleyebilir.

Teknik Detay: sayılar yaklaşık. Karmaşıklık token sayımıyla hesaplanıyor (if, döngüler, case, catch, &&, ||, ?? ve ?); arrow function'lar (fn) ayrı fonksiyon sayılmıyor, match kolları sayılmıyor. Kopya, yorumları ve PHP dışı HTML'i attıktan sonra altı satırlık birebir pencere arıyor; ajanın "tam aynı olmayan" kopyalarını yakalamaz. Skor ham çarpım, dosyalar arası normalize edilmiyor. Daha ince ölçüm için PHPStan, PHPMD gibi araçlar ya da Tornhill'in kurucusu ve CTO'su olduğu CodeScene var; bu betik ekstreyi okumak için, muhasebe için değil.

Ajana kart limiti: dört araçta kanca#

4.2'de bir ayrım yapmıştık: kural dosyası rica eder, kanca uygular. AGENTS.md'ye "fonksiyonları karmaşıklaştırma" yazmak bir temenni. Ajan her düzenlemeden sonra ölçülürse temenni kurala dönüşüyor.

Betiğin --tavan modu bunun için. Bilerek eski borcu affediyor: yalnızca değişen dosyalara bakıyor ve bir fonksiyon ancak tavanı aşıyorsa ve HEAD'deki hâlinden daha karmaşıksa (ya da yeni yazıldıysa) 2 ile çıkıyor. PHPMailer'ın preSend() fonksiyonuna bir yazım düzeltmesi için dokunan ajan durdurulmuyor; ona üç dal daha ekleyen ajan durduruluyor. Mevcut borcu bugün ödemek zorunda değilsiniz. Yeni kart çekmeyi durduruyorsunuz.

Altinkaynak'ın atılabilir bir kopyasında dört senaryoyla denedim (--tavan=10): temiz ağaç 0; Response.php içinde yalnızca boşluk değişikliği 0 (borç 14 ama büyümedi); decorateResponse() fonksiyonuna iki koşul eklenince 2 ve şu mesaj:

Bash
Borç tavanı aşıldı. Bu fonksiyonları bölmeden devam etme:
  src/Response/Response.php decorateResponse(): 16 (önce 14, tavan 10)

Karmaşıklığı 11 olan yeni bir dosya eklenince yine 2 ("önce yok"). Git deposu dışında 1.

Şimdi bunu ajanlara bağlayalım. Dört aracın dördünde de kanca var, ama ajanın sonucu nasıl gördüğü farklı:

  • Claude Code: .claude/settings.json içinde PostToolUse kancası, eşleştirici "Edit|Write". Dokümana göre bu kanca engelleyemez, düzenleme zaten yapılmıştır; ama 2 ile çıkarsa stderr Claude'a gösterilir. Yani ajan mesajı okur ve fonksiyonu bölmeye döner.
  • Codex: .codex/hooks.json ya da config.toml içinde [hooks] tablosu. Kanca dokümanına göre PostToolUse, dosya düzenleyen apply_patch aracını da kapsıyor; 2 ile çıkışta stderr geri bildirim olarak modele dönüyor. Kancalar varsayılan açık, ama proje içindeki kancalar ancak projenin .codex/ katmanına güvenilmişse yükleniyor.
  • Cursor: .cursor/hooks.json ("version": 1). Burada bir tuzak var: afterFileEdit kancası dokümanda biçimlendirici gibi işler için tarif ediliyor ve ajana bir şey döndürmüyor. Ajana söz geçirmek için stop kancası gerekiyor: döndürdüğü followup_message bir sonraki kullanıcı mesajı olarak gönderiliyor, varsayılan sınır beş döngü (loop_limit).
  • Aider: --test-cmd ile --auto-test her AI düzenlemesinden sonra komutu çalıştırıyor; dokümana göre sıfırdan farklı çıkışta Aider hatayı düzeltmeye çalışıyor.

Claude Code için ayar şöyle:

JSON
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "cd \"$CLAUDE_PROJECT_DIR\" && php borc-olcer.php --tavan=10"
          }
        ]
      }
    ]
  }
}

Aider tarafında tek satır:

Bash
aider --test-cmd "php borc-olcer.php --tavan=10" --auto-test

Acı Gerçek: bu kancaların hiçbirini bu yazı için ajanların içinde çalıştırmadım; betik ve dört senaryosu gerçek, ajanların kancaya verdiği tepki değil. Bir de daha sinsi bir sınır var: tavana takılan ajan karmaşıklığı düşürmenin en ucuz yolunu bulur. Kırk satırlık fonksiyonu anlamsız dört parçaya bölüp sayıyı tavanın altına çeker. Sayı düşer, okunurluk aynı kalır. Kanca size "burada borç büyüyor" diye haber verir; borcun gerçekten ödenip ödenmediğine yine bir insan bakar.

Ne zaman işe yarar, ne zaman yaramaz?#

Yarar: ajanla hızlı büyüyen bir kod tabanında, borcun nerede biriktiğini görmek için; elinizdeki sınırlı refactor zamanını nereye harcayacağınıza karar verirken; "bu modülü bir hafta temizlemeliyiz" talebini bir tabloyla savunmanız gerektiğinde. Kanca modu ise en çok ajanı gözetimsiz bıraktığınız işlerde işe yarar.

Yaramaz: geçmişi olmayan genç bir depoda faiz sütunu boş kalır; altinkaynak örneğinde gördüğünüz gibi her şey "dokunulmamış" görünür. Sayı tek başına hüküm de değildir: karmaşık bir para hesabı fonksiyonu olması gerektiği kadar karmaşık olabilir. Ve tavan, ajanın koda değil sayıya optimize etmesine kapı açar.

Borcu ölçmek ödemek değil. Ödemenin ilk adımı, borçlu kodun ne yaptığını değiştirmeden ona dokunabilmek; bunun için de önce testler gerekiyor. Serinin bir sonraki bölümü, Vibe Coder #5.2: AI-TDD – Önce Test, Sonra Sihir, testi sizin yazıp kodu ajana yazdırmanın sınırlarına bakacak. Eski bir binada borcun adım adım nasıl ödendiğini ise Kentsel Dönüşüm hattında, altinkaynak üzerinde izliyoruz. Serinin tamamı ve yayınlanan bölümlerin listesi Vibe Coder El Kitabı sayfasında.

AI size limitsiz bir kart verdi. Ekstreyi okumak hâlâ sizin işiniz. Teknolojiyle kalın.

Yorumlar

0 yorum

Yorum yazın

kişisine yanıt

Yorumlar onaylandıktan sonra görünür. E-posta adresiniz kimseyle paylaşılmaz. Kişisel verileriniz nasıl işlenir?