Files
kolaytercih/docs/gece-vardiyasi/2026-09-21/06-cto-plani.md
bilalgursen d10eff31b3 docs: gece vardiyası 2026-09-21 raporları
Çok ajanlı gece vardiyasının çıktıları: analitik (Rybbit) bulguları, SEO/GEO
konu planı, marka stratejisi, canlı mail testi, CEO kararları (2 tur), CTO
şartnamesi, yazar/yazılımcı raporları, bağımsız kod denetimi ve sabah özeti
(yazılı + sesli). Kod değişiklikleri gece/* dallarında.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 13:08:24 +03:00

33 KiB
Raw Blame History

06 — CTO planı — 2026-09-21 gece vardiyası (Tur 3)

Hazırlayan: CTO ajanı · Girdi: 05-ceo-kararlari.md §3 + 01/02/03/04 · Kod değiştirilmedi; yalnızca bu dosya yazıldı. Tüm dosya:satır atıfları main HEAD 56925e1 üzerinde okunarak doğrulandı. (Tek yan etki: p4-eposta worktree'sinde doğrulama için next typegen + tsc + lint koşuldu; yalnızca gitignore'lu .next/ ve next-env.d.ts oluştu, git status temiz.)

1. Özet (5 madde)

  1. Dört paket de uygulanabilir; dosya kümeleri ayrık. CEO'nun listesindeki üç gizli çakışma çözüldü: (a) P3'ün "tek sabit"i seo.tsx'e (P1'in) giremez → yeni dosya; (b) P2'nin paket_goruntulendi olayı /paket sayfasına (P3'ün) dokunmadan yeni src/app/paket/layout.tsx ile bağlanır; (c) P2 src/app/layout.tsx'e dokunmaz, erken olay kaybı analitik.ts içinde kuyrukla çözülür.
  2. P2 kök nedeni doğrulandı ve bir tane daha bulundu. Hook useRef + [ad, anahtarEki] deps'li effect kullanıyor; eleman ilk commit'te yoksa (banner) ya da sökülüp yeniden takılıyorsa (tadımlık) gözlem hiç kurulmuyor → callback ref. Ek bulgu: Rybbit script'i afterInteractive; olay() script yokken sessizce düşürüyor → mount anında atılacak yeni olaylar (sonuc_/paket_goruntulendi) sert yüklemede kaybolur → küçük kuyruk şart.
  3. P3'te CEO'nun kapsamı eksik (CTO itirazı #1). "İhtimal dili" yalnız riskNotuUret'te değil: tadımlık havuzunun riskNotu'su LLM üretimi ve DB'de kayıtlı, prompt'u açıkça "yüksek/orta/düşük ihtimal dili kullan" diyor (scripts/tadimlik-uret.ts:70-71); danışman sohbetinin sistem prompt'u da aynı (src/app/api/soru/route.ts:48). Girişsiz ziyaretçinin /sonuc'ta gördüğü tek yapay zekâ satırı eski dille kalırdı. Çözüm: risk cümlesi tek saf fonksiyona (riskNotuMetni) alınır, tadımlıkta okuma anında deterministik üretilir — prod'da LLM yeniden koşusu gerekmez.
  4. P4'te soğuma kaynağı değiştirildi (CTO itirazı #2). better-auth 1.6.23'te verification.identifier = token (e-posta değil); e-posta yalnız value JSON'unda ve satır sendMagicLink çağrılmadan önce yazılıyor → tablo tabanlı soğuma kendi satırına takılır ve kayan pencere üretir. Yerine süreç içi Map (tek konteyner; şema/DB yazımı yok). Ayrıca prod guard modül yüklenirken değil gönderim anında atmalı; aksi hâlde next build (NODE_ENV=production, env'siz) kırılır.
  5. Doğrulama tuzağı: taze worktree'de pnpm exec tsc --noEmit baseline'da kırık (PageProps global tipi .next/types'tan geliyor). Önce pnpm exec next typegen koşulmalı — denendi: typegen → tsc 0 hata, pnpm lint temiz. Tam pnpm build'i yalnız P1 koşar (HTML kabul kriteri); dört paralel build ~8 GB RAM ister, diğerleri tsc+lint ile kapanır, build'i Tur 4'te orkestratör sırayla alır.

Dosya sahipliği (ayrık; listede olmayan dosyaya dokunulmaz)

Paket Dosyalar (Y = yeni)
P1 gece/p1-rehber-altyapi src/features/rehber/components/el-cizimi-sema.tsx · src/features/rehber/components/rehber-makale.tsx · src/lib/rehber.ts · src/lib/tarih.ts Y · src/app/sitemap.ts · src/lib/seo.tsx (yalnız articleJsonLd)
P2 gece/p2-olcum src/components/use-gorunum-olayi.ts · src/lib/analitik.ts · src/features/sihirbaz/components/{hero-form,cta-sira-form,sihirbaz-cagri-karti}.tsx · src/features/liste/components/tercih-profili-kapisi.tsx · src/features/kullanici/components/giris-form.tsx · src/features/rapor/components/{liste-uretici,listem-govde}.tsx · src/app/paket/layout.tsx Y · src/features/odeme/components/paket-gorunum-olayi.tsx Y · src/components/rybbit-identify.tsx · src/features/kullanici/components/user-nav.tsx · (salt-okunur doğrulama, değişiklik beklenmiyor: kaydet-banner.tsx, tadimlik-satiri.tsx)
P3 gece/p3-vaat-dili src/app/page.tsx · src/app/paket/page.tsx · src/features/odeme/components/paket-satinal.tsx · src/lib/ai/rapor.ts (yalnız riskNotuUret) · src/app/layout.tsx (yalnız description) · src/lib/veri-kapsami.ts Y · CTO eki: src/lib/risk.ts · src/lib/tadimlik-havuzu.ts · scripts/tadimlik-uret.ts · src/app/api/soru/route.ts (yalnız 48. satır) · src/components/arama-funnel-karti.tsx (yalnız 38. satır) · src/features/katalog/components/veri-kaynagi.tsx (yalnız export)
P4 gece/p4-eposta src/lib/eposta-gonder.ts · src/lib/eposta.ts · src/lib/auth.ts · src/lib/analitik-sunucu.ts · isteğe bağlı ayrı commit: .gitea/workflows/ci.yaml

Çakışma kararları: seo.tsx → P1 (P3 oraya sabit eklemez). src/app/paket/page.tsx + paket-satinal.tsx → P3 (P2 yeni layout dosyası kullanır). src/app/layout.tsx → P3 (P2 script'e dokunmaz). tadimlik-satiri.tsx → P2, tadimlik-havuzu.ts → P3. analitik.ts → P2, analitik-sunucu.ts → P4. giris-form.tsx → P2, auth.ts → P4.

Tüm yazılımcılar için ortak kurallar

  • Yalnız kendi worktree + dalında lokal commit; push/merge yok. git add -A / git add . yasak — yalnız sahip olunan dosyalar yol vererek eklenir (build data/*.db-shm gibi izlenen dosyaları kirletebilir; data/app.db, docker-compose.yml, docs/geo/geo-raporu.md asla).
  • Doğrulama sırası (worktree kökünde): pnpm exec next typegen && pnpm exec tsc --noEmit && pnpm lint. Dev sunucusu/tarayıcı yok (AGENTS.md).
  • Next'e özgü API'den önce node_modules/next/dist/docs/ (P1: 01-app/03-api-reference/03-file-conventions/01-metadata/sitemap.md, 04-functions/generate-metadata.md; P2: 03-file-conventions/layout.md). Doğrulananlar: openGraph.modifiedTime tipte var (opengraph-types.d.ts:28); sitemap lastModified: string | Date; next typegen (06-cli/next.md:175).
  • Commit mesajı sonu: Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>. Rapor: 07-yazilimci-raporlari.md'ye kendi başlığı.

2. P1 · Rehber altyapısı

Amaç: Şema metni JS'siz botlara görünsün; mermaid hatasında sonsuz iskelet bitsin; guncelleme tarihi uçtan uca aksın; frontmatter tırnağı ve tablo taşması düzelsin.

Kök neden (kanıt):

  • el-cizimi-sema.tsx:85-97: aria-label yalnız svg state'i dolunca render ediliyor; SSR çıktısı = aria-hidden iskelet + figcaption. Bileşen client ama SSR edilir → içine konan <details> sunucu HTML'ine girer.
  • el-cizimi-sema.tsx:55-74: import("mermaid").then(async …) zincirinde catch yok; mermaid.render atarsa (getBBox is not a function, 2 oturum — tarayıcıya özgü, kaynağı saptanamadı) svg hiç dolmaz, iskelet kalır, promise yakalanmaz. Mermaid ayrıca #d<id> geçici div'ini body'de bırakabilir (mermaid.core.mjs:1248).
  • rehber.ts:71-74: .replace(/^["']|["']$/g, "") iki ucu bağımsız siliyor. Etkilenen yalnız 2 dosya (merkezi-yerlestirme…md:3, taban-siralamalar…md:3); 24 dosyanın hiçbirinde tam tırnaklı değer yok.
  • rehber.ts:20-26 RehberOzet'te guncelleme yok; sitemap.ts:22 /rehber girdisinde lastModified yok, :47 y.tarih; seo.tsx:149-169 dateModified yok; rehber-makale.tsx:32 yalnız publishedTime, gövdede tarih yok.
  • rehber-makale.tsx:79: [&_table]:w-full, sarmalayıcı yok.

Değişiklikler:

  1. src/lib/tarih.ts (yeni, saf, server-only yok): export function trTarih(iso: string): string — /^(\d{4})-(\d{2})-(\d{2})$/ ile ayrıştır, sabit AYLAR = ["Ocak",…,"Aralık"] dizisinden "21 Eylül 2026" üret; biçim uymazsa throw. Date/toLocale*/saat dilimi kullanma.
  2. src/lib/rehber.ts:
    • Tırnak: değer uzunluğu ≥ 2, ilk ve son karakter aynı tırnak (" ya da ') ve aradaki metin o tırnağı içermiyorsa soy; aksi hâlde olduğu gibi bırak.
    • RehberOzet'e guncelleme?: string. metaDogrula: tarih ve (varsa) guncelleme YYYY-MM-DD değilse ya da guncelleme < tarih ise build'i kıran Error (mevcut hata mesajı kalıbıyla). oku() ve ozetOku() (cache'ten kırpma dalı dahil, :169-170) alanı taşır. Sıralama tarih'e göre kalır. RehberAramaKaydi değişmez.
    • Tablolar: parcala() içindeki iki marked.parse çıktısını tek yardımcıdan geçir: <table> → <div class="rehber-tablo" role="region" aria-label="Tablo" tabindex="0"><table>, </table> → </table></div> (marked 18 <table>'ı özniteliksiz basar; global marked.use kullanma). :12 yorumundaki "10 makale"yi düzelt.
  3. src/lib/seo.tsx — yalnız articleJsonLd: dateModified?: string parametresi; çıktıda dateModified: opts.dateModified ?? opts.datePublished.
  4. src/features/rehber/components/rehber-makale.tsx: metadata'da openGraph: { type: "article", publishedTime: yazi.tarih, modifiedTime: yazi.guncelleme ?? yazi.tarih }; articleJsonLd'ye dateModified; aciklama paragrafının altına <p className="mt-3 text-xs text-slate-500">Yayın: <time dateTime={yazi.tarih}>{trTarih(yazi.tarih)}</time> + (yalnız guncelleme varsa) · Güncelleme: <time …> — düz metin satırı, badge değil (SectionEyebrow kuralı tetiklenmez). Sınıf listesinde [&_table]:mt-4 → [&_.rehber-tablo]:mt-4 [&_.rehber-tablo]:overflow-x-auto.
  5. src/app/sitemap.ts: rehber girdilerinde lastModified: y.guncelleme ?? y.tarih; /rehber girdisini staticEntries'ten fonksiyon içine al, lastModified = tüm yazıların guncelleme ?? tarih değerlerinin en büyüğü (ISO string karşılaştırması yeter).
  6. src/features/rehber/components/el-cizimi-sema.tsx:
    • const [hata, setHata] = useState(false). cizdir() → import("mermaid").then(async …).catch(() => { document.getElementById("d" + semaId)?.remove(); if (!iptal) setHata(true); }); mermaid.initialize'a suppressErrorRendering: true (11.16'da var, config.type.d.ts:237).
    • Render: svg varsa mevcut role="img" aria-label div'i (korunur); yoksa ve hata değilse iskelet; hata ise iskelet çizilmez. figcaption'dan sonra, <figure> içinde: <details open={hata || undefined} className="mt-2 text-xs text-slate-500"><summary className="cursor-pointer font-medium text-slate-600">Şemayı metin olarak oku</summary><p className="mt-2 text-sm leading-6 text-slate-600">{ariaLabel}</p></details>. Gizli metin (sr-only, display:none) yok.

Commit bölünmesi: (1) fix(rehber): frontmatter tırnak + tablo sarmalayıcı · (2) feat(rehber): guncelleme alanı → sitemap/JSON-LD/OG/görünür tarih · (3) fix(rehber): şema metni SSR'da + mermaid hata toparlama. Hepsi [KARAR].

Kabul kriterleri / doğrulama: typegen+tsc+lint; ardından yalnız bu pakette pnpm build, sonra (ana repodaki mevcut build'de baseline ölçüldü: ilk iki grep 0, description Üst sıraya… diye tırnaksız başlıyor — yani kriterler ayırt edici):

  • grep -c "Şemayı metin olarak oku" .next/server/app/rehber/tercih-listesi-nasil-yapilir.html ≥ 1 ve aynı dosyada grep -c "Tercih listesi dilim şeması" ≥ 1; .next/server/app/meraklisina.html'de grep -c "Sihirbaz akış şeması" ≥ 1.
  • grep -o 'name="description" content="[^"]\{0,30\}' .next/server/app/rehber/merkezi-yerlestirme-nasil-calisir.html → &quot;Üst sıraya ile başlar (aynısı taban-siralamalar-nasil-degisir).
  • Aynı HTML'de rehber-tablo sınıfı ve Yayın: + <time dateTime="2026- bulunur; .next/server/app/sitemap.xml.body'de /rehber</loc> girdisinin <lastmod>'u var. Geçici olarak bir yazıya guncelleme: 2026-09-22 ekleyip dateModified/modifiedTime/Güncelleme: çıktısını gör, commit'lemeden geri al (content/ yazar dalının).
  • Kod incelemesi: .catch var, hata dalında iskelet yok, details açık.

Kapsam dışı: kume/ilgili (T4), FAQPage, llms.txt, IndexNow, flowchart dışı mermaid, font, yazar alanı, content/rehber/* (yazar dalı), rehber-listesi.tsx.

Riskler / AGENTS.md: ElCizimiSema /meraklisina hunisiyle ortak → <details> orada da çıkar; metin kaynağı yine tek ariaLabel prop'u olduğundan şema senkron kuralına yeni yük binmez, huni-semasi.tsx'e dokunulmaz; Bilal görsel bakmalı. Ekran okuyucuda metin iki kez okunabilir (CEO kabul etti). metaDogrula sıkılaşması: 24 dosyanın hepsi YYYY-MM-DD (doğrulandı), yazar dalındaki yeni dosyalar bozuk tarihle gelirse build kırılır — istenen davranış.


3. P2 · Ölçüm onarımı

Amaç: Görünüm hook'u sonradan mount olan elemanda da çalışsın; 6 yeni olay; 429 mesajı; PII ayrı commit.

Kök neden (kanıt):

  • use-gorunum-olayi.ts:20,28-30,51: useRef + deps [ad, anahtarEki]. ref.current effect anında null ise erken dönüyor ve deps değişmediği için bir daha çalışmıyor.
    • kaydet-banner.tsx:33,47,57: kapali başlangıçta true → ilk render null; üstelik içerik ViewportPortal içinde (viewport-portal.tsx — ilk client render'da da null). Eleman hiçbir zaman ilk commit'te yok → olay 0.
    • tadimlik-satiri.tsx:75-77,87,104: tip uymuyorsa setSatir(null) → <section> sökülür, fetch sonrası yeni DOM düğümü takılır; gözlemci eski (kopuk) düğümde kalır.
    • Çalışanlar (listem-govde.tsx:139-148, sihirbaz-cagri-karti.tsx:232) eleman ilk commit'te var olduğu için etkilenmiyor.
  • İkinci neden (raporlarda yok): layout.tsx:129-136 script'i afterInteractive; analitik.ts:52-58 window.rybbit yoksa sessiz no-op. Sert sayfa yüklemesinde mount effect'i script'ten önce koşar → olay kaybolur. Mevcut olaylar kullanıcı etkileşimiyle atıldığı için görünmedi; yeni "sayfa görüntülendi" olaylarını doğrudan vurur.
  • Uyarı: tadimlik_goruntulendi = 0'ın tek nedeni hook olmayabilir — prod tadimlik_havuzu boşsa bileşen hiç render edilmez (tadimlik-bolumu.tsx:20, bellek notu "deploy sonrası prod'da pnpm tadimlik") ve bölüm uzun tablonun altında. Düzeltmeden sonra hâlâ 0 ise önce havuz kontrol edilir (Bilal).

Değişiklikler:

  1. src/lib/analitik.ts: OlayAdi'na tam bu adlarla ekle: cta_form_goruntulendi, sira_form_hata, sonuc_goruntulendi, paket_goruntulendi, liste_uretim_hata, giris_hata. olay(): window.rybbit yoksa olayı modül içi kuyruğa (en çok 20) koy; 500 ms aralıkla en çok 20 deneme (10 sn) — script gelince sırayla boşalt, gelmezse kuyruğu at (reklam engelleyici). Yeni export function olayBirKez(ad, ozellikler?, anahtarEki?): hook'taki sessionStorage anahtar mantığının (kolaytercih.olay.<ad>[.<ek>]) aynısı — anahtar varsa çık, yoksa yaz + olay(); storage hatasında yine gönder.
  2. src/components/use-gorunum-olayi.ts: dönüş tipi RefCallback<T>; useCallback((el: T | null) => { … }, [ad, anahtarEki]). el yoksa / IntersectionObserver yoksa / anahtar zaten yazılıysa çık; yoksa gözlemci kur, kesişimde io.disconnect() + olayBirKez. React 19 ref temizleme fonksiyonu döndür (return () => io.disconnect()). ozelliklerRef deseni korunur. Çağıranların hepsi yalnız ref={…} veriyor (.current okuyan yok — doğrulandı) → 5 kullanım yerinde değişiklik gerekmez.
  3. cta-sira-form.tsx: yalnız profilsiz daldaki <section>'a (:162) ref={useGorunumOlayi<HTMLElement>("cta_form_goruntulendi")} (hook çağrısı koşulsuz, üstte). Özellik yok — Rybbit pathname'i zaten yazar. catch dalına (:110) olay("sira_form_hata", { kaynak: "cta" }).
  4. hero-form.tsx:87 catch → { kaynak: "hero" }; tercih-profili-kapisi.tsx facetYukle catch → { kaynak: duzenle ? "duzenle" : "kapi" }. Doğrulama hatası (geçersiz sayı) sayılmaz, yalnız istek hatası.
  5. sihirbaz-cagri-karti.tsx SihirbazBolumu: mount effect'inde olayBirKez("sonuc_goruntulendi", { girisli: girisliMi ? "evet" : "hayir", sira_kovasi: siraKovasi(sira) }, girisliMi ? "girisli" : "anonim").
  6. /paket: src/features/odeme/components/paket-gorunum-olayi.tsx (yeni, client, null döner) — effect'te new URLSearchParams(window.location.search).get("kaynak"), allowlist kilit | kredi_bitti | nav, değilse "diger"; olayBirKez("paket_goruntulendi", { kaynak }, kaynak). useSearchParams kullanma (cacheComponents: true → Suspense/CSR bailout derdi). src/app/paket/layout.tsx (yeni): {children} + <PaketGorunumOlayi />. Linkler: listem-govde.tsx:288,369 → /paket?kaynak=kilit; liste-uretici.tsx:134 → ?kaynak=kredi_bitti; user-nav.tsx:51 → ?kaynak=nav.
  7. liste-uretici.tsx: else dalı (:87) olay("liste_uretim_hata", { kod: sonuc.code }); catch (:89) { kod: "ISTISNA" }. AUTH/KREDI/PAKET dalları bu olayı atmaz.
  8. giris-form.tsx: linkGonder hata dalı → olay("giris_hata", { yontem: "eposta", status: error.status ?? 0 }); status === 429 ise toast "Çok sık denedin, bir dakika sonra tekrar dene", değilse mevcut mesaj. Google hata dalı → { yontem: "google", status }. geriSayimBaslat 30 → 60 sn (P4'ün sunucu soğumasıyla hizalı; P4 merge edilmese de zararsız).
  9. Ayrı, son commit [BİLAL]: rybbit-identify.tsx eposta/isim prop'ları ve trait'ler kalkar, kullaniciTanit(kullaniciId); user-nav.tsx:47 çağrısı sadeleşir. analitik.ts'e dokunmaz.

Commit bölünmesi: (1) fix(analitik): görünüm hook'u callback ref + erken olay kuyruğu · (2) feat(analitik): huni olayları (3–7 + 8'in olay kısmı) · (3) fix(giris): 429 mesajı + 60 sn geri sayım · (4) [BİLAL] chore(gizlilik): rybbit identify yalnız kullanıcı ID. (4) en sonda durmalı: user-nav.tsx'te 47. ve 51. satırlar yakın; Bilal reddederse git merge gece/p2-olcum~1.

Kabul kriterleri: typegen+tsc+lint temiz. grep -rn "useGorunumOlayi" src → mevcut 5 + cta-sira-form. grep -rnE "olay(BirKez)?\(" src | grep -iE "email|eposta:|sira:" → boş (ham sıra/e-posta yok; sıra yalnız siraKovasi()). Olay adları yukarıdaki listeyle birebir (kalıcı sözleşme). Kod incelemesi: hook eleman null→düğüm geçişinde gözlem kuruyor, düğüm sökülünce disconnect ediyor.

Kapsam dışı: giris_basarili (P4), odeme_hata, sihirbaz_kapatildi, rapor_yazdirildi, /sonuc pageview şişmesi, iç trafik filtresi, src/app/layout.tsx, page.tsx/sohbet-client.tsx/tercih-degisti-modali.tsx içindeki /paket linkleri (kaynak = diger düşer), rapor-actions.ts (hata alt nedeni).

Riskler / AGENTS.md: Yeni CTA butonu eklenmiyor; var olan ArrowRight desenine dokunma. ?kaynak= /paket canonical'ını etkilemez (sayfaMetadata path: "/paket"). /giris?callback=/paket dönüşünde kaynak kaybolur (kabul). Kuyruk sayesinde dev'de (script yok) 10 sn sonra sessizce boşalır — hata yok.


4. P3 · Vaat dili ve güven temizliği

Amaç: Olmayan özelliği ("yerleşme olasılığı") satan dili kaldırmak; 4↔5 yıl tutarsızlığını bitirmek; iki [BİLAL] değişikliğini bağımsız commit olarak hazırlamak.

Kök neden (kanıt): page.tsx:180, paket-satinal.tsx:31 "yerleşme olasılığı"; rapor.ts:166,168,170,172 dört cümlenin dördü de "ihtimal". "4 yıl": page.tsx:107,122,148,180,365 + paket-satinal.tsx:31; DB sira2021…sira2025 = 5 (rapor.ts programBilgisi, tablolar "Son 5 yıl"). CEO listesinde olmayanlar: scripts/tadimlik-uret.ts:70-71 (prompt "ihtimal dili kullan"), :72 ("son 4 yılın"), :84-87 (mock); src/app/api/soru/route.ts:48; src/components/arama-funnel-karti.tsx:38 "Yerleşme ihtimalini görerek seç." Tadımlık riskNotu DB'de (schema.ts:187) ve tadimlik-havuzu.ts:83'ten olduğu gibi servis ediliyor; oysa rozet aynı satırda canlı hesaplanıyor (tadimlik-satiri.tsx:107) → metinle rozet çelişebiliyor. CEO'nun grep'i (olasılı|ihtimalle yerleş) "ihtimalini/ihtimal dili/ihtimali düşük" kalıplarını yakalamıyor.

Commit 1 — [KARAR] fix(metin): olasılık dili + 5 yıl:

  • src/lib/veri-kapsami.ts (yeni): export const VERI_YIL_SAYISI = 5; (+ yorum: DB sira2021–sira2025; veri yılı eklenince güncelle). seo.tsx'e koyma (P1'in).
  • src/lib/risk.ts: export function riskNotuMetni(risk: RiskSeviyesi | null): string — güvenli: "Son taban sıralaması senin sıranın belirgin altında; geçmiş veriye göre güvenli aralıkta." · az-riskli: "Son taban sıralaması sırana yakın; küçük bir kayma sonucu değiştirebilir." · riskli: "Son taban sıralaması senin sıranın üstünde kapandı; hayal tercihi olarak düşün." · null: "Güncel taban verisi yok; bu satırı temkinli değerlendir." ("Geçen yıl" deme: kod sira2025 ?? sira2024'e düşüyor.)
  • src/lib/ai/rapor.ts riskNotuUret: gövde → return riskNotuMetni(riskHesapla(p.sira2025 ?? p.sira2024, adaySira));. Başka hiçbir şeye dokunma (5/13/6 şeması, risk eşiği, prompt aynı).
  • page.tsx:180 ve paket-satinal.tsx:31 → `Her tercih için yerleşme riski ve trend analizi (son ${VERI_YIL_SAYISI} yıl)`; page.tsx:107,122,148,365 sabitten. page.tsx'te import'u 32. satırın (@/lib/credits) hemen altına koy (commit 3'ün dokunacağı 15. ve 55-59. satırlardan uzak).
  • arama-funnel-karti.tsx:38 → "Yerleşme riskini görerek seç."

Commit 1b — [KARAR · CTO eki] fix(ai): tadımlık risk notu deterministik + prompt dili:

  • tadimlik-havuzu.ts:83: riskNotu: riskNotuMetni(riskHesapla(program.efektifSira, sira) ?? dilimdenRisk(satir.dilim)) (program = satırın snapshot'ı; üç fonksiyon @/lib/risk). Böylece /api/tadimlik de kapsanır; DB satırları ve şema değişmez.
  • scripts/tadimlik-uret.ts:70-72 prompt: "riskNotu: geçmiş taban verisine dayalı risk değerlendirmesi (TEK kısa cümle). ASLA garanti/kesin/yüzde/olasılık/ihtimal deme; 'güvenli / az riskli / riskli' etiketleriyle konuş." + "son 5 yılın"; mock metinleri aynı dile. Script koşulmaz.
  • api/soru/route.ts:48 → "ASLA yerleşme garantisi verme; olasılık, yüzde ya da 'ihtimal' dili kullanma — güvenli / az riskli / riskli etiketleri ve taban sıralarıyla konuş."

Commit 2 — [BİLAL] chore(metin): danışman fiyat kıyası: "2.000–10.000 TL" → "1.000–5.000 TL" (page.tsx:90,168,484, paket/page.tsx:52); "onda bir fiyatına" → "çok daha düşük fiyata" (paket/page.tsx:49, layout.tsx:55; page.tsx:228 yorum satırı içinde ölü kod — yorum bloğunu sil). Fiyat bölümü paragrafının (page.tsx:484-487) altına text-xs text-slate-500 kaynak satırı: "Danışman fiyat aralığı: Armut, 2026." Sabit kullanma, düz metin (commit 1'den bağımsız kalsın). Rakamı ajanlar doğrulamadı → Bilal Armut sayfasını açmadan almasın.

Commit 3 — [BİLAL] chore(guven): hero'da YÖK logosu yerine tarihli veri satırı: veri-kaynagi.tsx:4 const TARIH_METNI → export const KATALOG_TARIH_METNI (dosya içi kullanım güncellenir; tek biçim kaynağı). page.tsx:236-251: <Image> + dikey ayraç kalkar; aynı kutu (inline-flex … rounded-lg border … sınıfları aynen) içinde <strong>Veri: YÖK Atlas</strong> · son güncelleme <time dateTime={KATALOG_VERI_TARIHI}>{KATALOG_TARIH_METNI}</time>. import Image (15) silinir; KATALOG_VERI_TARIHI 55-59'daki seo import'una eklenir. public/yok-logo.svg silinmez.

Kabul kriterleri: typegen+tsc+lint. Commit 1+1b sonrası: grep -rniE "olasılı|ihtimal" src scripts/tadimlik-uret.ts → yalnız page.tsx:76 ("binlerce ihtimal" — kombinasyon anlamı, vaat değil), page.tsx:132 ("boşta kalma ihtimalini" — SSS, kapsam dışı), program-tablosu.tsx:176 (kod yorumu) ve prompt'lardaki yasaklayıcı cümleler. grep -rn "4 yıl" src/app/page.tsx src/features/odeme → boş. Commit 2 sonrası grep -rn "onda bir\|2\.000–10\.000" src → boş. Bağımsızlık testi: main'den geçici dal açıp commit 2'yi ve 3'ü tek başlarına git cherry-pick et → çakışmasız + tsc geçer; geçici dalı sil.

Kapsam dışı: Garanti→Güvenli (H2) — cta-sira-form.tsx'teki "garanti dilimleri / nereye yerleşirsin" dahil (dosya P2'nin); /hakkinda, "Sınırlarımız", hero/kapanış tonu, SSS metinleri, vizyon.md.

Riskler / AGENTS.md: Davranış değişmiyor (eşikler, 5/13/6, havuz kuralları aynı) → huni-semasi.tsx SEMA güncellenmez; şemadaki "risk notu gerçek sıralama geçmişinden hesaplanır, yapay zekâ yorumu değildir" iddiası 1b ile tadımlık için de doğru hâle gelir. Kayıtlı raporlardaki eski cümleler kalır (göç yok). Commit 3 yeni badge tasarlamaz (SectionEyebrow kuralı: mevcut bilgi kutusu, pill değil). CTA butonlarına dokunulmuyor.


5. P4 · Giriş e-postası sağlamlaştırma

Amaç: Reply-To, prod'da sessiz fallback'in bitmesi, adres başına soğuma, şablon temizliği, giris_basarili.

Kök neden (kanıt): eposta-gonder.ts:13-18 ve auth.ts:33-37 NODE_ENV'e bakmadan konsola yazıp başarı dönüyor (token'lı link loga düşer); eposta-gonder.ts:22 sessiz onboarding@resend.dev; :21-29 replyTo yok; eposta.ts:79-80 Google Fonts <link>, :96-101 footer'da iletişim yok; auth.ts'te adres bazlı sınır yok (yalnız eklentinin IP limiti 5/60 sn, magic-link/index.mjs:157-163); analitik-sunucu.ts:17 tek olay.

Değişiklikler:

  1. eposta-gonder.ts: export function epostaKonsolModu(): boolean = !process.env.RESEND_API_KEY && (process.env.NODE_ENV !== "production" || process.env.EPOSTA_KONSOL_FALLBACK === "1"). epostaGonder başında, çağrı anında (modül seviyesinde asla — next build NODE_ENV=production + env'siz koşar, modül yüklenirken atmak build'i kırar): konsol modu → mevcut console.log; anahtar yok ve konsol modu değil → throw new Error("[eposta] RESEND_API_KEY tanımlı değil"); EMAIL_FROM yok → prod'da (kaçış bayrağı yoksa) throw, aksi hâlde mevcut resend.dev. send'e replyTo: process.env.EMAIL_REPLY_TO ?? "destek@kolaytercih.com" (Resend 6 alan adı replyTo).
  2. auth.ts sendMagicLinkEmail: :33-37 bloğu → if (epostaKonsolModu()) { console.log(link); return; }; aksi hâlde epostaGonder (yanlış yapılandırmada atar → better-auth 500 → formda hata toast'ı; P2 giris_hata{status:500} yazar). __sonMagicLink satırı yerinde kalır.
  3. Soğuma — auth.ts, yalnız NODE_ENV === "production" (dev paneli "anında giriş" art arda istek atıyor): globalThis.__girisEpostaSon: Map<string, number>; anahtar email.trim().toLowerCase(); her çağrıda 60 sn'den eski kayıtları buda; kayıt varsa sessizce return (mail yok, istek 200); yoksa zamanı yaz → gönder; gönderim atarsa kaydı sil (kullanıcı hemen yeniden deneyebilsin) ve hatayı yeniden fırlat. Konsol modunda da çalışır (lokal docker testi görsün).
  4. eposta.ts: :79-80 iki <link> satırı silinir; footer'a (:98-99) üçüncü satır: <a href="https://kolaytercih.com" style="color:#94a3b8;">kolaytercih.com</a> &middot; <a href="mailto:destek@kolaytercih.com" style="color:#94a3b8;">destek@kolaytercih.com</a>; düz metin (text) gövdelerinin sonuna aynı iki bilgi. Satış dili ekleme.
  5. analitik-sunucu.ts:17: "odeme_tamamlandi" | "giris_basarili". auth.ts databaseHooks'a session: { create: { after: async (session, ctx) => { void sunucuOlayi("giris_basarili", { kullaniciId: session.userId, pathname: "/giris", ozellikler: { yontem: ctx?.path?.startsWith("/magic-link") ? "eposta" : ctx?.path?.startsWith("/callback") ? "google" : "diger" } }); } } } — await yok; imza (session, context | null) 1.6.23 tiplerinde doğrulandı (@better-auth/core … init-options.d.mts). E-posta özelliğe girmez.
  6. İsteğe bağlı commit [BİLAL] .gitea/workflows/ci.yaml: .env.production bloğuna RYBBIT_HOST, RYBBIT_SITE_ID, RYBBIT_API_KEY, EMAIL_REPLY_TO satırları (secret yoksa boş gelir → kod no-op/varsayılan; zararsız). Gerekçe aşağıda, itiraz #4.

Commit bölünmesi: (1) chore(eposta): şablondan harici font + footer iletişim · (2) fix(eposta): Reply-To + prod'da sessiz fallback yok · (3) feat(giris): adres başına 60 sn soğuma · (4) feat(analitik): giris_basarili sunucu olayı · (5) [BİLAL] ci.yaml. Tamamı "Bilal görmeden merge edilmez"; (2) ve (3) canlı giriş yolunu değiştirir, tek tek atılabilir.

Kabul kriterleri: typegen+tsc+lint. grep -n "fonts.googleapis\|fonts.gstatic" src/lib/eposta.ts → boş. grep -n "process.env" src/lib/eposta-gonder.ts → tüm okumalar fonksiyon gövdesinde. grep -n "await sunucuOlayi" src/lib/auth.ts → boş. Kod incelemesi: prod + anahtarsız + bayraksız yolda hiçbir console.log URL basmıyor. Bilal'in merge öncesi lokal docker testi: EPOSTA_KONSOL_FALLBACK=1 ile link konsola düşer, 60 sn içinde ikinci istek link üretmez; bayraksız/anahtarsız → formda hata.

Kapsam dışı: disposable filtre, vazgeçme linki/List-Unsubscribe, eski token iptali, Resend webhook, Traefik IP başlığı / better-auth rateLimit ayarı, DNS, kredi-hatirlatma.ts.

Riskler: Hata = kimse giriş yapamaz → (2)-(3) için lokal docker testi şart (bellek notu "testler lokal docker'la"). Süreç içi Map yeniden başlatmada sıfırlanır ve çoklu kopyada paylaşılmaz — bugün tek konteyner (docker-compose.yml), ikinci katman IP limiti duruyor. Soğumada düşen isteğin verification satırı yetim kalır (15 dk'da ölür, hiç postalanmadı — zararsız). session.create Google girişinde ve dev panelde de tetiklenir (dev'de env yok → no-op).


6. CTO itirazları (CEO kararına)

  1. P3 kabul kriteri eksik ve grep'i yetersiz. Yalnız riskNotuUret düzeltilirse girişsiz /sonuc tadımlığı (LLM üretimi, DB'de kayıtlı) ve danışman sohbeti "ihtimal" diliyle kalır; prompt'lar bu dili emrediyor. Alternatif: commit 1b (okuma anında deterministik risk notu + iki prompt satırı). Prod'da pnpm tadimlik --force gerekmez.
  2. P4-3 "kaynak: verification tablosu" kırılgan. identifier = token (magic-link/index.mjs:61-68), e-posta yalnız value JSON'unda (indeks yok, name ile biçim değişir), satır sendMagicLink'ten önce yazılıyor (kendine takılır), düşürülen her istek yeni satır bırakıp pencereyi kaydırır; düzeltmesi tabloya DELETE gerektirir. Alternatif: süreç içi Map — şema yok, DB yazımı yok, better-auth iç yapısına bağımlılık yok.
  3. P4-2 "net hata" modül seviyesinde atılamaz — next build kırılır; yalnız gönderim anında.
  4. P4-5 giris_basarili prod'da büyük olasılıkla hiç düşmeyecek: sunucuOlayi RYBBIT_HOST/SITE_ID/API_KEY ister (analitik-sunucu.ts:13-15,31); CI'ın yazdığı .env.production'da bu üçü yok (.gitea/workflows/ci.yaml:17-34). Rybbit'teki 3 odeme_tamamlandi kaydının kaynağı bu yüzden şüpheli (lokal docker testi olabilir). Bilal: Gitea secret'larını ekle + P4 commit 5'i al; yoksa huninin giris_basarili adımı boş kalır.
  5. P2 paket_goruntulendi {kaynak} CEO'nun dosya listesiyle yapılamaz (/paket sayfası ve paket-satinal.tsx P3'ün; SatinAlForm yalnız girişliye render oluyor). Alternatif: yeni paket/layout.tsx. Ana sayfadaki iki /paket linki (page.tsx:535,565, P3'ün) kaynak taşıyamaz → diger.
  6. P2'ye erken-olay kuyruğu eklenmezse yeni görüntülenme olayları sert yüklemede (OAuth dönüşü, e-postadan gelen, yer imi) sistematik eksik sayar; CEO listesinde yoktu, eklendi.
  7. İstemci 30 sn ↔ sunucu 60 sn: kullanıcı 30. sn'de "Yeniden gönder"e basınca sunucu sessizce yutar, kullanıcı mail bekler. P2 geri sayımı 60'a çeker.
  8. "Her paket pnpm build": dört paralel build ≈ 8 GB (bellek notu: build ~2 GB). Yalnız P1 build eder; diğerleri typegen+tsc+lint; build Tur 4'te sırayla.

7. Backlog (bu gece yapılmayan teknik maddeler, öncelik sırasıyla)

  1. Prod tadımlık havuzu kontrolü (pnpm tadimlik) — P2 yayına girip tadimlik_goruntulendi hâlâ 0 ise ilk şüpheli. [Bilal]
  2. Gitea secret'ları: RYBBIT_*, EMAIL_REPLY_TO (itiraz #4). [Bilal]
  3. Garanti→Güvenli (H2) tek paket: ~10 dosya + SEMA + aria + figcaption aynı commit; cta-sira-form.tsx "garanti dilimleri / nereye yerleşirsin" ve page.tsx:132 dahil. Karar bekliyor.
  4. Rehber T4 (kume/ilgili + /rehber küme başlıkları, SectionEyebrow ile) — P1 merge'inden sonra, aynı dosyalar.
  5. liste_uretim_hata alt nedeni: rapor-actions.ts sonuç tipine neden: "ai" | "dogrulama" | "tekrar"; ardından odeme_hata, sihirbaz_kapatildi, rapor_yazdirildi; kalan /paket linklerine kaynak (page.tsx, sohbet-client.tsx, tercih-degisti-modali.tsx).
  6. Tarih biçimi tekilleştirme: veri-kaynagi.tsx'in toLocaleDateString'i P1'in trTarih'ine geçsin (P1+P3 merge sonrası); hydration #418 şüphesini de kapatır.
  7. better-auth rateLimit + advanced.ipAddress.ipAddressHeaders Traefik'e göre sabitlenmesi (IP başlığı yanlışsa limit ya herkese ortak ya hiç işlemiyor) — canlı ayar, lokal docker + Bilal.
  8. error.tsx'e ChunkLoadError için tek seferlik reload; /sonuc pageview şişmesi (replaceState).
  9. el-cizimi-sema.tsx:14-15 font lisansı — alınmadıysa font değişimi işi.
  10. Eski token iptali, Resend webhook/alarm, hatırlatma retry, disposable filtre (ürün/hukuk kararı sonrası).