# 08 — Kod denetimi (CTO / bağımsız denetçi) — 2026-09-21 Kapsam: `gece/p1-rehber-altyapi`, `gece/p2-olcum`, `gece/p3-vaat-dili`, `gece/p4-eposta` (hepsi `56925e1` tabanlı). Kod **değiştirilmedi**, commit atılmadı. Yöntem: `git diff 56925e1..` + worktree'den tam dosya okuma + kütüphane kaynağı (`better-auth 1.6.23` magic-link / `with-hooks`, `resend 6.18`) + scratchpad'de `tsx` betikleri (P1 parser'ı 24 ve 27 dosyalık iki içerik kümesinde; P4 guard matrisi; `docker compose config` ile env dosyası ayrıştırma) + `sqlite3 -readonly` (`data/yokatlas.db`, `data/app.db`). Şeffaflık notu: `entegrasyon` worktree'sinde yanlışlıkla iki salt-okunur git komutu (`git log`, `git status --short`) çalıştırdım; dosya değişmedi, build'e etkisi beklenmez. ## 1. Hükümler | Dal | Hüküm | Gerekçe | |---|---|---| | **P1** `gece/p1-rehber-altyapi` | **MERGE EDİLEBİLİR** | Parser 24 mevcut + 3 yeni yazıda (7 `guncelleme` alanı dahil) hatasız koştu; yalnız hedeflenen 2 açıklama değişti; hydration/catch/sitemap mantığı temiz. | | **P2** `gece/p2-olcum` | **MERGE EDİLEBİLİR** | Callback ref + React 19 temizliği, kuyruk zamanlayıcısı ve PII kuralları doğru; yalnız düşük önemli ölçüm sapması riskleri var. Son commit (identify) Bilal kararı. | | **P3** `gece/p3-vaat-dili` | **DÜZELTMEYLE** | İlk iki commit temiz; `52520ac` doğrulanmamış bir rakamı "Armut, 2026" diye kaynak göstererek yayına sokuyor — doğrulanmadan alınmamalı. Ayrıca tadımlık `gerekce`/`trendOzeti` DB metinleri eski dille kalıyor (paket amacı eksik kapanıyor). | | **P4** `gece/p4-eposta` | **MERGE EDİLEBİLİR (koşullu)** | Guard mevcut prod env'inde (anahtar + `EMAIL_FROM` dolu) girişi kırmıyor; `EMAIL_FROM` fiilen zaten zorunluydu. Koşul: lokal docker testi + P2'nin `e1bb482` commit'iyle **birlikte** merge (yoksa 30. sn'de "Yeniden gönder" sessizce yutulur). | Kritik bulgu: **yok.** Yüksek bulgu: **1** (P3 `52520ac`). ## 2. Bulgular (en ciddiden) | # | Dal + commit | Dosya:satır | Sorun | Somut hata senaryosu | Önem | Önerilen düzeltme | Kesinlik | |---|---|---|---|---|---|---|---| | 1 | P3 `52520ac` [Bilal] | `src/app/page.tsx:478-480` (+ `:92,168,474`, `src/app/paket/page.tsx:52`) | Doğrulanmamış fiyat aralığı gerçek bir şirkete atfediliyor: "Danışman fiyat aralığı: Armut, 2026." Yazılımcı raporu rakamın açılıp teyit edilmediğini kendisi söylüyor. | Merge → ana sayfada üçüncü tarafa atıflı, kaynağı olmayan rakam yayında; Armut sayfası farklı aralık gösteriyorsa yanıltıcı karşılaştırmalı reklam / yanlış atıf. | **yüksek** | Bu commit'i Bilal kaynağı açıp teyit edene kadar alma. `9d450c5` ondan bağımsız: `git merge-tree --merge-base=52520ac cae2aaa 9d450c5` temiz → `git merge cae2aaa` + `git cherry-pick 9d450c5`. Teyit edilirse kaynak satırına URL/erişim tarihi ekle. | DOĞRULANDI (kod + rapor) | | 2 | P3 `cae2aaa` | `src/lib/tadimlik-havuzu.ts:84-93` | Yalnız `riskNotu` deterministik yapıldı; aynı satırın `gerekce` ve `trendOzeti` alanları hâlâ DB'deki LLM metni. Yorumdaki "olasılık dili sızamaz" iddiası yanlış. | Lokal `data/app.db` `tadimlik_havuzu`: 514 satırın **13**'ünde `gerekce` "yerleşme olasılığı yüksektir / yüksek ihtimal" içeriyor, **126**'sında `trend_ozeti` "Son 4 yılda…" diyor. Girişsiz ziyaretçi /sonuc'ta "yerleşme olasılığı yüksektir" + sayfanın başka yerinde "son 5 yıl" görür. | orta | Ya `pnpm tadimlik --force` (yeni prompt'la) prod'da koşulmalı (Bilal; bellek notundaki prod koşusu zaten bekliyor), ya da okuma anında `gerekce`/`trendOzeti` için `/olasılı|ihtimal/i` eşleşen satırlar atlanmalı / `trendOzeti` de `trendOzetiUret` benzeri deterministik üretilmeli. Yorum satırı düzeltilmeli. | DOĞRULANDI (lokal DB); prod içeriği OLASI | | 3 | P4 `2af4a9f` × P2 `e1bb482` | `src/lib/auth.ts:66-77` × `giris-form.tsx:41-44` | Paketler arası bağımlılık: sunucu 60 sn soğumada isteği sessizce 200 ile yutuyor. P2'nin 60 sn geri sayımı yoksa istemci 30. sn'de yeniden göndermeye izin veriyor. | P4 tek başına merge → kullanıcı 30. sn'de "Yeniden gönder" → "gönderildi" ekranı, mail çıkmaz, kullanıcı gelmeyecek maili bekler. (İkisi birlikteyken: istemci sayacı yanıt geldikten sonra, sunucu damgası istek başında başlar → istemci 60 sn ≥ sunucu 60 sn, tutarlı.) | orta (yalnız ayrı merge edilirse) | P2'yi P4'ten **önce ya da aynı anda** merge et. Kalan kenar durum (sayfa yenileme / ikinci cihazdan 60 sn içinde tekrar istek → sessiz başarı) kabul edilebilir: ilk mail zaten gitti ve 15 dk geçerli. | DOĞRULANDI | | 4 | P4 `5992cf3` | `src/lib/eposta-gonder.ts:47-55` | Prod guard'ın 500 döndürdüğü koşullar (betikle denendi): (a) `RESEND_API_KEY` yok **ya da boş string** ve `EPOSTA_KONSOL_FALLBACK≠1`; (b) anahtar var ama `EMAIL_FROM` yok/boş. | Gitea'da `RESEND_API_KEY` ya da `EMAIL_FROM` secret'ı silinir/yeniden adlandırılırsa CI satırı `EMAIL_FROM=` (boş) yazar → her magic-link isteği 500, e-postayla giriş kapalı (Google etkilenmez). **Bugünkü prod'da kırılma beklenmez:** `04-mail-testi.md` canlı mailin `giris@mail.kolaytercih.com`'dan geldiğini gösteriyor → iki secret da dolu. Eski kodda da boş `EMAIL_FROM` `??`'yi geçip Resend'e `from:""` gidiyordu → zaten hata; yani `EMAIL_FROM` **yeni zorunlu hale gelmedi**, yalnız hata görünür oldu. CI her iki satırı da yazıyor (`ci.yaml:27-28`), Dockerfile env gömmüyor, runtime `env_file: .env.production`. | düşük (bilgi) | Merge öncesi Gitea'da `RESEND_API_KEY` ve `EMAIL_FROM` secret'larının dolu olduğunu gözle teyit et (değerine bakmadan, varlığına). Lokal docker testi şartname §5'teki gibi. | DOĞRULANDI | | 5 | P2 `cf19903` | `src/features/sihirbaz/components/cta-sira-form.tsx:80,168` | `useTercihProfili` `useSyncExternalStore` + sunucu snapshot'ı `null`: hydration'da **profilli** ziyaretçi için de önce profilsiz `
` bağlanır, sonra senkron yeniden render ref'i düşürür. IO ilk bildirimi ile React'in passive-effect görevi arasında sıra garantisi yok. | Kayıtlı profilli ziyaretçi, CTA kartı ilk ekranda görünen bir sayfayı sert yüklerse `cta_form_goruntulendi` yanlışlıkla 1 kez atılabilir → huni giriş adımı şişer. Kart çoğu sayfada fold altında olduğundan nadir. | düşük | IO callback'inde göndermeden önce `el.isConnected` + profil yokluğunu kontrol et ya da ref'i `profil === null && hydrated` koşuluna bağla. Merge'i engellemez; Rybbit'te `cta_form_goruntulendi / sira_girildi{cta}` oranı izlenmeli. | OLASI (yazılımcı da çalıştırarak doğrulamadı) | | 6 | P2 `fa43392` (kapsam dışı kalan kardeş sorun) | `src/lib/analitik.ts:170-181`, `rybbit-identify.tsx` | Kuyruk yalnız `olay()` için; `kullaniciTanit()` script yokken hâlâ sessiz no-op. | OAuth / magic-link dönüşü (sert yükleme): identify effect'i script'ten önce koşar → o sayfa oturumunda kullanıcı eşlenmez; kuyruktan boşalan `sonuc_goruntulendi{girisli:evet}` anonim profile yazılır, P4'ün sunucu `giris_basarili{user_id}` olayıyla aynı profilde birleşmez. Regresyon değil (öncesinde de böyleydi) ama yeni huninin son adımını zayıflatır. | düşük-orta | Backlog: identify'ı da aynı kuyruğa al (tek son-değer slotu yeter). | OLASI | | 7 | P4 `17046c8` | `src/lib/auth.ts:161-181`, `analitik-sunucu.ts:34-52` | Doğruluk tarafı temiz: `void` + `sunucuOlayi` içi `try/catch` + env yoksa erken `return` → giriş bozulamaz, unhandled rejection yok; `ctx.path` magic-link'te `/magic-link/verify`, Google'da `/callback/:id` (kaynakta doğrulandı); hook `queueAfterTransactionHook` içinde koşuyor. **Ama** olay sunucunun IP/UA'sıyla (`user-agent: node`) gidiyor. | Rybbit bot filtresi bu UA'yı düşürürse ya da olayı ayrı bir "sunucu oturumu"na yazarsa huni adımı tarayıcı oturumuyla bağlanmaz. `odeme_tamamlandi` de aynı yoldan; CTO zaten o 3 kaydın kaynağından şüpheli. | düşük | Secret'lar eklendikten sonra Bilal bir test girişiyle Rybbit'te `giris_basarili`'nin düştüğünü ve kullanıcı profiline bağlandığını doğrulasın; bağlanmıyorsa `/api/track` gövdesine istekten `user_agent` (ve gerekiyorsa `ip_address`) geçirilmesi ayrı iş. | OLASI | | 8 | P4 `2af4a9f` | `src/lib/auth.ts:41-43` | Normalizasyon `trim().toLowerCase()`; Gmail nokta/`+etiket` eşlemesi yok. (Boşluklu adres zaten `z.email()`'de reddediliyor, `trim` fiilen no-op.) | `kurban+1@gmail.com`, `kurban+2@…` ile aynı kutuya soğuma aşılır; yine de IP limiti (5/60 sn) sınırlar. Bellek: her çağrıda budama → Map ≤ son 60 sn'deki adres sayısı, sızıntı yok. Yeniden başlatmada sıfırlanır, çoklu kopyada paylaşılmaz (tek konteyner — kabul). Google OAuth yolu `sendMagicLink`'e hiç girmiyor → etkilenmez. | düşük | Şimdilik kabul; istenirse gmail/googlemail için `+` sonrası ve noktalar atılabilir. | DOĞRULANDI | | 9 | P1 `1a75f42` | `src/features/rehber/components/el-cizimi-sema.tsx:109-125` | `
` artık `
`'ün ortasında (iskelet/svg → figcaption → `
`); HTML içerik modeline göre figcaption ilk ya da son çocuk olmalı. | Tarayıcılar tolere eder, hydration'ı etkilemez; bazı doğrulayıcı/erişilebilirlik araçları uyarı verir. | düşük | `
`'i figcaption'dan **önce** koy ya da figcaption'ın içine al. | DOĞRULANDI | | 10 | P3 `16c4508` | `src/lib/veri-kapsami.ts:6` | `VERI_YIL_SAYISI = 5` kolon kapsamıyla tutarlı (`sira2021…sira2025`). Ancak 23.610 programın yalnız **9.909**'unda beş yılın tamamı dolu (`sira2021` dolu: 11.450; `sira2025` dolu: 17.994). | "Her tercih için … trend analizi (son 5 yıl)" — 2025 tabanı olan programların ~%45'inde fiilen 5'ten az yıl var. Eski "4 yıl" da aynı ölçüde yaklaşık bir ifadeydi; regresyon değil. | düşük | İstenirse metin "son 5 yıla kadar" yapılabilir; Bilal kararı. | DOĞRULANDI (`sqlite3 -readonly`) | | 11 | P1 `d9e125c` | `src/lib/rehber.ts:145` | `TARIH_BICIMI` yalnız biçim denetliyor: `2026-02-31` geçer (ay 13 ise `trTarih` build'i kırar, gün için kontrol yok). | Yazar `2026-09-31` yazarsa sitemap/JSON-LD'ye geçersiz tarih çıkar. Bugünkü 27 dosyada yok. | düşük | `Date.UTC` ile gün-ay geçerlilik kontrolü eklenebilir. | DOĞRULANDI | ### Özellikle bakılıp **temiz** bulunanlar - **P1 parser:** P1'in `rehber.ts`'i hem kendi 24 dosyasında hem `gece/rehber-yazilari`'nın 27 dosyasında (7 `guncelleme`) hatasız; tüm `tarih`/`guncelleme` geçerli, `guncelleme ≥ tarih`. Tırnak değişikliği yalnız hedeflenen iki `aciklama`'yı etkiliyor (başka tırnak uçlu frontmatter değeri yok). Tablo sarmalayıcı: 8 tabloda aç/kapa sayıları eşit; içerikte ham `` riski yok). `
` sunucu ve ilk istemci render'ında aynı → hydration uyuşmazlığı yok. `.catch` hem `import()` hem `render` hatasını yakalıyor, `iptal` korumalı, geçici `#d` temizleniyor. Sitemap `lastmod` = `guncelleme ?? tarih`, `/rehber` = en büyük ISO string; yazı yoksa `undefined` (alan yazılmaz). `sayfaMetadata` `openGraph`'ı yayarak birleştiriyor → `modifiedTime` çıkar. - **P2 hook/kuyruk:** callback ref, React 19 temizlik fonksiyonu döndürüyor; erken dönüşlerde `undefined` → React sökmede `ref(null)` çağırır, `!el` ile karşılanıyor. StrictMode çift mount: gözlemci `disconnect` + yeniden kurulur; `olayBirKez` sessionStorage ile tekilleşir. `setInterval` her iki çıkışta (`kuyruguBosalt`, deneme sınırı) temizleniyor; en çok 20 olay × 10 sn. `paket/layout.tsx` sunucu bileşeni, dinamik API yok; `useSearchParams` kullanılmıyor → `/paket` statikliği etkilenmez; `/paket` altında başka rota yok (olay yalnız `/paket`'te atılır). PII: olay özellikleri yalnız `kaynak`, `kod`, `status`, `yontem`, `girisli`, `sira_kovasi`; ham sıra/e-posta yok. `SihirbazBolumu` yalnız `/sonuc`'ta kullanılıyor → `sonuc_goruntulendi` adı doğru. - **P3 risk:** `riskNotuMetni` doğrudan `riskHesapla` çıktısını tüketiyor → eşikler rozetle birebir (1,4× / ≥ sıra). Tadımlık yolu `riskHesapla(efektifSira, sira) ?? dilimdenRisk(dilim)` — `tadimlik-satiri.tsx:107` rozetiyle aynı ifade; `efektifSira` eksik/null → dilimden türetilir, `null` metne düşmez. `program` kolonu rozet tarafında zaten aynı şekilde okunuyor (yeni null riski yok). `rapor.ts`'te yalnız `riskNotuUret` gövdesi + import değişti; LLM şeması/prompt'u (5/13/6) aynı. `tadimlik-uret.ts` ve `api/soru` değişiklikleri yalnız prompt metni. - **P4 env dosyası:** yeni yorum satırları (apostrof ve `→` içeren) `docker compose config` ile denendi → doğru ayrıştırılıyor; boş secret'lar `""` geliyor, kod `||` ile varsayılana düşüyor. `replyTo` alan adı Resend 6.18 tipinde var. Modül seviyesinde `throw` yok → `next build` env'siz kırılmaz. `kredi-hatirlatma.ts` yeni `throw`'u mevcut `try/catch` ile yutuyor. - **Paketler arası sembol çakışması yok:** `giris_hata` (P2, istemci `OlayAdi`) ile `giris_basarili` (P4, `SunucuOlayAdi`) ayrı tipler; `trTarih` (P1) ile `KATALOG_TARIH_METNI` (P3) bağımsız; P2 `paket/layout.tsx` + P3 `paket/page.tsx` ayrık dosyalar. - **AGENTS.md:** yeni badge/CTA/modal yok; "Yayın:" satırı düz metin; hero kutusu mevcut kutu; sihirbaz davranışı değişmedi → `SEMA` güncellemesi gerekmiyor; `/meraklisina`'da çıkan `
` metni tek kaynak `ariaLabel`'dan geliyor. ## 3. Bilal için önerilen merge sırası 1. **`gece/rehber-yazilari` + `gece/p1-rehber-altyapi`** birlikte (P1 olmadan `guncelleme` alanları görünmez; P1'in doğrulaması yeni frontmatter'larla sınandı — temiz). 2. **`gece/p2-olcum`** — ilk üç commit (`fa43392`, `cf19903`, `e1bb482`). `c055e82` (identify'dan e-posta/isim kaldırma) Bilal kararı; reddedilirse `git merge gece/p2-olcum~1`. Not: Rybbit'te önceden yazılmış e-posta/isim trait'leri kod değişikliğiyle silinmez. 3. **`gece/p3-vaat-dili`** — `git merge cae2aaa` (ilk iki commit) + istenirse `git cherry-pick 9d450c5` (YÖK logosu→metin; çakışmasız doğrulandı). **`52520ac`'yi Armut rakamı teyit edilmeden alma** (bulgu #1). Deploy sonrası prod'da tadımlık havuzu yeni prompt'la yeniden üretilmeli (bulgu #2). 4. **`gece/p4-eposta`** — en son, P2 merge edildikten sonra. Önce lokal docker testi (şartname §5: `EPOSTA_KONSOL_FALLBACK=1` ile link konsolda, 60 sn içinde ikinci istek link üretmez; bayraksız/anahtarsız → formda hata). Gitea'da `RESEND_API_KEY` + `EMAIL_FROM` secret'larının dolu olduğunu teyit et. `ea3ba5f` (ci.yaml) + `RYBBIT_*` secret'ları eklenmeden `giris_basarili` prod'da no-op kalır (zararsız). 5. Push öncesi AGENTS.md kuralı: birleşik HEAD temiz worktree'de `pnpm build`.