Ç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>
181 lines
33 KiB
Markdown
181 lines
33 KiB
Markdown
# 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` → `"Ü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> · <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ı).
|