Files
kolaytercih/docs/gece-vardiyasi/2026-09-21/06-cto-plani.md
bilalgursen 0ab4dbe88c
All checks were successful
Deploy / deploy (push) Successful in 7m18s
Enhance Traefik configuration for www.kolaytercih.com
Added a new router and middleware for handling www to apex domain redirection, ensuring that Let's Encrypt certificates are obtained for both domains. Updated the docker-compose.yml file to include necessary rules and middleware for proper redirection and TLS handling.

Also, updated the CEO agent description to remove redundant details and clarified the QA engineer's responsibilities regarding testing critical flows and error reporting. Adjusted the CTO agent's section for consistency in expert guidance and clarified the designer's tools to include 'Skill' for better design resource management.
2026-09-21 20:37:12 +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ı).