Files
kolaytercih/docs/ekip-raporlari/2026-10-07/cto-geri-bildirim-sartname.md
bilalgursen da0e9dff7a docs(ekip): 7–10 Ekim raporları, eval sonuçları, seslendirme betiği
- docs/ekip-raporlari/2026-10-07, 2026-10-09, 2026-10-10 raporları ve ekran görüntüleri
  (ses/ altındaki mp3 dosyaları depoya alınmadı, 38 MB)
- BACKLOG #38: rehber yazılarına "Dinle" düğmesi adayı
- scripts/seslendir.py (Türkçe TTS) ve .gitignore'a .venv-tts/
- scripts/eval/sonuc: 8–10 Ekim revizyon eval çıktıları

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 00:33:39 +03:00

370 lines
46 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CTO şartnamesi — Bilal'in 10 maddelik geri bildirimi (7 Eki 2026)
Taban: dal `feat/arc-secim-sayi`, HEAD `4d30dd1`. Tüm dosya:satır atıfları bu commit üzerinde doğrulandı. Çalışma ağacında yalnız `data/app.db` kirli (Bilal'in); dokunulmaz.
## Özet (5 madde)
1. **Beş paket, dosya kümeleri ayrık:** A (sohbet sunucusu: rehber linki doğrulama + revizyon sınırının kaldırılması), B (revizyon "ilk N" garantisi), C (program varyant etiketi: burs/dil/öğretim türü), D1 (sohbet UI: shimmer, logo varyantı, soru geçişi), D2 (program ayrıntı çekmecesi: "Yıllara göre" kaldırma, açılış bug'ı, logo→içerik koreografisi). Madde 7 için iş yok.
2. **Üç kök neden koddan doğrulandı:** (1) rehber linki için kod tarafında hiçbir doğrulama yok — denetim yalnız vaat/kayıt/revizyon beyanına bakıyor (`src/lib/ai/cikti-denetimi.ts`), prompt "adres uydurma" diyor ama model uyduruyor; (2-3) `programs.isim` sütunu burs/dil varyantını taşımıyor (Başkent Tıp ×6 satır: Burslu/%50/Ücretli × Türkçe/İngilizce; ayırt edici alanlar `program_ozellik.burs_orani / ogrenim_dili / ogrenim_turu`), hiçbir liste bu alanı göstermiyor; (4) "ilk N" kuralı modelin seçimine bırakılmış — `secimiOnar` yalnız eksik satırı tamamlarken +4 öncelik veriyor, model 24 satırı İstanbul'suz doldurursa `ilkTercihleriOneAl` öne alacak satır bulamıyor; dev mock yolu (`mockRapor`) kuralı hiç uygulamıyor.
3. **İtiraz (madde 8-9):** "Drawer'daki info" liste çekmecesinde değil; `liste-cekmecesi.tsx`'te bilgi paneli yok. Info düğmesi ve "Yıllara göre" bölümü `src/features/katalog/components/program-ayrinti.tsx` (ProgramAyrintiButonu; rapor-listesi.tsx:364 ve program-tablosu.tsx:443'ten açılıyor). Açılış bug'ının kök nedeni çift durak (0,6 → 0,92) + veri geldikten sonra boyun değişmesi (alt-sayfa.tsx ResizeObserver yeniden hedefliyor, kısık durakta gövde kaydırmıyor).
4. **Test altyapısı:** vitest/jest yok; mekanik testler `scripts/eval/*.ts` içinde, `pnpm tsx --conditions=react-server <betik>` ile koşuyor ve çıkış kodu 1 ile düşüyor (`akis-denetim.ts` deseni: sahte OpenAI ucu + geçici app.db kopyası). Yeni testler bu desene uyar: `baglanti-denetim.ts`, `revizyon-denetim.ts`, `varyant-denetim.ts`.
5. **Merge sırası:** paketler bağımsız; tek bilinçli çakışma A↔D1 (`sohbet-client.tsx`'in `revizyonHakki` prop'u) — A prop'u 0/1 bayrak olarak besler, D1 dokunmaz, entegrasyonda orkestratör 3 satırlık yeniden adlandırma yapar. Build'i yalnız orkestratör alır; paketler `typegen + tsc + lint` ile kapanır.
## Dosya sahipliği
| Paket | Madde | Dosyalar (Y = yeni) |
|---|---|---|
| **A** sohbet sunucusu | 1, 6 | `src/lib/ai/baglanti-denetimi.ts` (Y), `src/lib/ai/sohbet-akisi.ts`, `src/app/api/soru/route.ts`, `src/lib/ai/danisman-prompt.ts`, `src/lib/ai/araclar.ts`, `src/lib/ai/sohbet-meta.ts`, `src/lib/credits.ts`, `src/features/rapor/liste-revizyon.ts`, `src/features/rapor/components/listem-icerik.tsx`, `src/features/rapor/components/listem-govde.tsx`, `src/features/rapor/components/sohbet-senaryo-verisi.ts`, `src/app/page.tsx`, `src/features/odeme/components/paket-satinal.tsx`, `scripts/eval/akis-denetim.ts`, `scripts/eval/baglanti-denetim.ts` (Y) |
| **B** revizyon "ilk N" | 4 | `src/lib/revizyon-kurali.ts`, `src/lib/ai/rapor.ts`, `scripts/eval/revizyon-denetim.ts` (Y) |
| **C** program varyantı | 2, 3 | `src/types/yokatlas.ts`, `src/lib/db.ts`, `src/lib/program-varyant.ts` (Y), `src/lib/rapor-havuzu.ts`, `src/components/liste-satiri.ts`, `src/features/liste/liste-queries.ts`, `src/features/liste/liste-dogrula.ts`, `src/features/liste/hooks/use-tercih-profili.ts`, `src/features/liste/components/program-ekle-butonu.tsx`, `src/features/liste/components/program-tablosu.tsx`, `src/features/liste/components/liste-cekmecesi.tsx`, `src/features/rapor/components/rapor-listesi.tsx`, `src/features/rapor/rapor-queries.ts`, `src/features/katalog/components/universite-program-tablosu.tsx`, `src/features/katalog/components/bolum-program-listesi.tsx`, `scripts/eval/varyant-denetim.ts` (Y) |
| **D1** sohbet UI | 5, 10 | `src/features/rapor/components/sohbet-client.tsx`, `src/features/rapor/components/sohbet-anketi.tsx`, `src/lib/adim-gecisi.ts` (Y), `src/features/sihirbaz/components/sihirbaz-adimlar.tsx`, `src/components/ui/loader.tsx` (yalnız gerekirse, bkz. D1-5) |
| **D2** program ayrıntı çekmecesi | 8, 9 | `src/features/katalog/components/program-ayrinti.tsx`, `src/features/katalog/components/akademik-ayrinti.tsx`, `src/components/ui/acilis-sahnesi.tsx` (Y), `docs/tasarim/DESIGN.md` (yalnız §4.19) |
| — | 7 | iş yok |
Dokunulmayanlar (bilerek): `src/lib/ai/cikti-denetimi.ts` (link denetimi ayrı saf modülde; vaat/kayıt denetimleriyle karışmasın), `src/features/rapor/rapor-actions.ts` (`revisionCount` sayaç olarak kalır), `src/lib/appdb/schema.ts` (göç yok), `src/features/rapor/components/arac-pilleri.tsx`, `src/lib/ai/cikti-denetimi.ts`, `src/components/animasyonlu-mark.tsx`, `src/components/ui/alt-sayfa.tsx`.
## Çakışma kararları
1. **`sohbet-client.tsx` — A ve D1.** Sahibi **D1**. A, `listem-govde.tsx:450`'de `revizyonHakki={kalanHak}` yerine `revizyonHakki={hasPaket ? 1 : 0}` geçer ve yanına `// 0/1 bayrak; prop adı entegrasyonda revizyonAcik olur` yorumu koyar. D1 prop'a dokunmaz. Entegrasyonda orkestratör `revizyonHakki?: number` → `revizyonAcik?: boolean` yeniden adlandırmasını (sohbet-client.tsx:352,385,944,1075 + listem-govde.tsx:450) tek commit'te yapar.
2. **`src/lib/ai/rapor.ts` — B ve C.** Sahibi **B**. C, `programBilgisi` (rapor.ts:∼140) fonksiyonuna dokunmaz; varyant etiketi kayıtlı rapora yazılmaz, okuma anında `rapor-queries.ts`'te zenginleştirilir (eski kayıtlara da işler — bkz. C-4).
3. **`src/lib/ai/danisman-prompt.ts`, `araclar.ts`, `route.ts` — A tek sahip.** Madde 1'in prompt tarafı ve madde 6'nın prompt/araç/route değişiklikleri aynı pakette olduğu için çakışma yok.
4. **`docs/tasarim/DESIGN.md` — D2 tek sahip** (yalnız §4.19). D1'in §4.20'ye eklemesi gereken bir not varsa raporuna yazar, tasarımcı/orkestratör entegrasyonda işler.
5. **`rapor-havuzu.ts` — C.** B bu dosyaya dokunmaz (B'nin işi `rapor.ts` + `revizyon-kurali.ts`).
## Tüm paketler için ortak kurallar
- Yalnız verilen worktree + dalda **lokal** commit; push/merge yok. `git add -A` / `git add .` yasak; dosyalar yol vererek eklenir. Yıkıcı git komutları (`reset --hard`, `checkout --`, `restore`, `clean`, `stash`) yasak (AGENTS.md).
- Worktree'de `node_modules` **symlink değil klon** (`cp -Rc node_modules <worktree>/node_modules`); Turbopack symlink'te patlıyor.
- Doğrulama sırası, her commit'ten önce: `pnpm exec next typegen && pnpm exec tsc --noEmit && pnpm lint`. (`package.json`'da `typegen` script'i yok; `pnpm exec next typegen` kullanılır. Taze worktree'de typegen koşulmadan `tsc` baseline'da kırıktır — `PageProps` tipi `.next/types`'tan gelir.) `git status` temiz kalmalı; typegen yalnız gitignore'lu çıktı üretir.
- **`pnpm build` koşulmaz** (paralel build RAM'i tüketir); tam build'i entegrasyonda orkestratör alır. `next build` env'siz koşar: modül yüklenirken env zorunlu kılan kod yazılmaz; guard çağrı anında atar.
- Dev sunucusu / tarayıcı yok. Next'e özgü bir API'ye dokunmadan önce `node_modules/next/dist/docs/` içindeki sayfa okunur; A için `01-app/03-api-reference/03-file-conventions/route.md` (Route Handler) ve `01-app/01-getting-started/15-route-handlers.md`.
- AGENTS.md / DESIGN.md: rozet = `SectionEyebrow`; CTA'da yıldız yok, `ArrowRight` sonda; kapatma çarpısı `DialogContent` sınıf seti; `dark:` yazılmaz, yüzey `bg-card`; `pnpm tasarim:denetim` sıfır hata (lint koşuyor). Sihirbaz/liste mimarisi değişmiyor → `/meraklisina` şeması bu turda değişmez (B'nin "ilk N" garantisi havuz kuralını değil seçim onarımını değiştirir; şema zaten "aday havuzu dışına çıkılmaz" diyor).
- Mekanik testler `scripts/eval/` altında, `pnpm tsx --conditions=react-server <betik>` ile koşar, beklenti tutmazsa `process.exit(1)`; desen `scripts/eval/akis-denetim.ts`. Testler `data/app.db`'yi doğrudan **kullanmaz** — geçici kopya (`copyFileSync` + `APP_DB_URL=file:…` + `node scripts/db-goc.mjs`).
- Her paket raporu: değişen dosyalar, komut çıktıları (typegen/tsc/lint + paket testi), "koddan çıkarım" işaretli kalanlar, backlog önerisi.
---
## Paket A — Sohbet sunucusu: rehber linki doğrulama (madde 1) + revizyon sınırı kaldırma (madde 6)
### A-0 Kök neden (doğrulandı)
- **Madde 1.** `danisman-prompt.ts:247-251` modele "adres araç sonucundan gelir, adres uydurma" diyor; `rehber_ara`/`rehber_oku` (`araclar.ts:304, 378, 1288-1314, 1476-1492`) gerçek slug'ları döndürüyor. Ama `sohbet-akisi.ts:207-247` çıktı denetimi yalnız vaat dili, kayıt beyanı, revizyon beyanı ve şema sızıntısına bakıyor; `/rehber/<slug>` için hiçbir kontrol yok. Model araç çağırmadan ezberden `[Yatay geçiş](/rehber/yatay-gecis-rehberi)` yazabiliyor; istemci (`sohbet-baglanti.tsx:58,78`) her `/rehber/[a-z0-9-]+` adresini "Rehber yazısı" hapı yapıyor → 404'e giden hap. Gerçek slug kaynağı `content/rehber/*.md` (50 dosya) → `getAllRehberSlugs()` / `getRehberAramaKayitlari()` (`src/lib/rehber.ts:224-264`, `server-only`).
- **Madde 6.** `MAX_REVIZYON = 2` (`credits.ts:18`); kapı `liste-revizyon.ts:82-88`; prompt `danisman-prompt.ts:75-82`; araç `araclar.ts:472` ("en fazla 2 hak"), `araclar.ts:1764,1770`; route `route.ts:25,199,210`; UI `listem-icerik.tsx:12,180`, `listem-govde.tsx:78,102,219,335,450`, `sohbet-client.tsx:352,385,944,1075`; pazarlama `page.tsx:31,247`, `paket-satinal.tsx:37`; demo `sohbet-senaryo-verisi.ts:78,90`; tip `sohbet-meta.ts:50` (`kalanHak`). DB sütunu `reports.revision_count` (`schema.ts:165`, `drizzle/0000_baslangic.sql:62`) — **göç gerekmez**: sütun sayaç olarak kalır (kaç kez kuruldu, istatistik), yalnız kapı olmaktan çıkar.
### A-1 Rehber linki doğrulama — tasarım
Akış `text-delta` parçalarıyla istemciye anında gidiyor (`sohbet-akisi.ts:189-194`); cevap bitince eklenen "düzeltme notu" yanlış hapı ekrandan geri alamaz. Bu yüzden düzeltme **akış sırasında** yapılır: küçük bir tampon, bir bağlantı adayı başladığında metni tutar, bağlantı kapanınca doğrulayıp bırakır. Bağlantılar kısadır (≤ ~120 karakter), gecikme hissedilmez.
**Yeni saf modül `src/lib/ai/baglanti-denetimi.ts`** (DB/ağ/Next yok; `server-only` import etmez — `sohbet-akisi.ts` gibi betiklerden koşar):
```ts
export type RehberKaydi = { slug: string; baslik: string };
export type BaglantiDuzeltme = { slug: string; sonuc: "yakin" | "duz"; yeniSlug?: string };
/** Metindeki /rehber/<slug> adreslerini (markdown bağlantısı, ters tırnak ve çıplak adres) gerçek kayıtlarla eşler. */
export function rehberBaglantilariniDuzelt(metin: string, rehberler: readonly RehberKaydi[]): { metin: string; duzeltmeler: BaglantiDuzeltme[] };
/** Akış tamponu: delta verilir, güvenle bırakılabilen metin döner; bitir() kalanı boşaltır. */
export class BaglantiTamponu {
constructor(rehberler: readonly RehberKaydi[]);
besle(delta: string): string;
bitir(): string;
readonly duzeltmeler: BaglantiDuzeltme[];
}
```
Kurallar:
- Eşleme: slug `content/rehber` kaydıysa dokunma. Değilse **en yakın gerçek slug**: slug'lar `-` ile parçalanır, Dice/Jaccard benzerliği hesaplanır; eşik ≥ 0,5 **ve** tek aday açık ara önde (ikinciden ≥ 0,15 fazla) ise `[başlık](/rehber/yeni-slug)` olarak yeniden yazılır (başlık kayıttan). Aksi hâlde markdown bağlantısı **düz metne** iner (`[Yatay geçiş](/rehber/x)` → `Yatay geçiş`), çıplak `/rehber/x` adresi silinir (öncesindeki "bkz." gibi bağlaç kalır; aşırı mühendislik yok).
- Tampon: metinde `[` ya da `/rehber/` görülünce o noktadan itibaren tutulur; markdown bağlantısı `)` ile, çıplak adres slug dışı bir karakterle (boşluk, nokta, satır sonu, `)`) kapanınca doğrulanıp bırakılır. `[` sonrası 160 karakter içinde `](` gelmezse tutulan metin olduğu gibi bırakılır (sıradan köşeli parantez). `bitir()` kalanı doğrulayıp boşaltır.
- `/universite/…`, `/bolum/…` bu turda doğrulanmaz (backlog).
**`sohbet-akisi.ts` değişikliği:**
- `danismanAkisi` seçeneklerine `rehberler?: readonly RehberKaydi[]` eklenir (verilmezse tampon pasif: betikler/evaller eski davranışı korur).
- Okuma döngüsünde (`:181-195`): `text-start` → yeni `BaglantiTamponu`; `text-delta` → `cikti = tampon.besle(parca.delta)`; `metin += cikti`; `cikti` boş değilse `writer.write({ ...parca, delta: cikti })`; `text-end` → `kalan = tampon.bitir()`; kalan varsa önce `text-delta` olarak yazılır, sonra `text-end`. Diğer parçalar olduğu gibi.
- `CiktiDenetimi`'ne `baglantiDuzeltme: number` eklenir; `console.info("[soru] rehber bağlantısı düzeltildi:", n)` loglanır (slug'lar loglanabilir, kişisel veri değil).
- **Kayıt** (`onTamam(metin)`) düzeltilmiş metni alır — ekran ve DB aynı.
**`route.ts`:** `import { getRehberAramaKayitlari } from "@/lib/rehber"` ve `danismanAkisi({ ..., rehberler: getRehberAramaKayitlari() })`. (Prod'da cache'li, `rehber.ts:52-60`.)
**`danisman-prompt.ts` BAĞLANTI maddesi (`:247-251`):** bir cümle eklenir: "Rehber adresi yalnız bu cevapta rehber_ara / rehber_oku sonucunda gördüğün `sayfa` alanından gelir; görmediğin bir yazıya bağlantı verme, önce rehber_ara çağır." (Kod koruması asıl savunma; prompt yalnız araç çağrısını teşvik eder.)
### A-2 Revizyon sınırının kaldırılması
- `credits.ts`: `MAX_REVIZYON` silinir (yorumuyla).
- `liste-revizyon.ts`: `:82-88` HAK kapısı silinir; `RevizyonSonucu.kod` birliğinden `"HAK"` çıkar; `ozetCikar` imzasından `revisionCount` ve dönen `kalanHak` çıkar (`:46-57`); `:40,43,126,160` metinlerindeki "revizyon hakkın kullanılmadı" → "kredin/paketin etkilenmedi" (dürüst: paketlide bedel 0). `revisionCount: +1` güncellemesi **kalır** (`:155`).
- `sohbet-meta.ts:50`: `kalanHak` tipten çıkar (eski kayıtlardaki fazla alan çalışma zamanında zararsız).
- `araclar.ts`: `:472` açıklamasında "(paketli adayda, en fazla 2 hak)" → "(paketli adayda; sınır yok)"; `:1764` HAK dalı silinir; `:1770` "kalan revizyon hakkını belirt" ibaresi silinir; `AracBaglami.kalanRevizyon` (`:103-104`) silinir.
- `danisman-prompt.ts:75-82` `revizyonBolumu(r: { paketli: boolean }, params)`: `kalanHak` dalları silinir; paketli durum metni: "Revizyon sınırsız; her kurulum 20-60 sn sürdüğü için isteği yine tek seferde doğru kur." 3. maddedeki "kalan hakkı belirt" silinir. `sistemPromptu` seçeneği `revizyon?: { paketli: boolean }` olur.
- `route.ts`: `MAX_REVIZYON` import'u ve `:199,210` hesapları silinir; `revizyon: { paketli }`; `aracBaglami.kalanRevizyon` kalkar.
- `listem-icerik.tsx:12,180`: `MAX_REVIZYON` import'u ve `kalanHak` prop'u kalkar. `listem-govde.tsx`: `kalanHak` prop'u (`:78,102`) silinir; `:219` "(kalan hak: N)" ifadesi silinir; `:335` koşulu `hasPaket` olur; `:450` → `revizyonHakki={hasPaket ? 1 : 0}` + yorum (çakışma kararı 1).
- `sohbet-senaryo-verisi.ts:78` `kalanHak: 1` ve `:90` "Kalan revizyon hakkın: 1." silinir.
- `page.tsx:31,247`: `MAX_REVIZYON` import'u kalkar; madde metni "Sınırsız liste revizyonu (danışmanla sohbetten)". `paket-satinal.tsx:37`: aynı metin. `paket/page.tsx:13` ve `page.tsx:185` zaten "liste revizyonları" diyor, dokunulmaz.
- `sohbet-akisi.ts:134-137` "bu cevapta ikinci revizyon çalıştırılmaz" korunur — bu bir hak değil, cevap başına döngü koruması.
### A-3 Kabul kriterleri
1. `grep -rn "MAX_REVIZYON\|kalanHak\|kalanRevizyon" src scripts` → 0 satır.
2. `grep -rn "revizyon hakk" src` → 0 satır (eval raporları ve docs hariç).
3. `pnpm tsx --conditions=react-server scripts/eval/baglanti-denetim.ts` → `… senaryo geçti`, çıkış 0. Senaryolar (en az): (a) gerçek slug'lı markdown bağlantısı aynen kalır; (b) `/rehber/yatay-gecis-rehberi` → `/rehber/yatay-gecis-cap-yandal` (yakın eşleşme, başlık kayıttan); (c) `/rehber/tamamen-uydurma-seyler` → düz metin; (d) ters tırnak içi ve çıplak adres de düzeltilir; (e) tampon: aynı bağlantı 3-4 deltaya bölünerek beslenince `besle` çıktılarının birleşimi + `bitir()` tek seferlik sonuçla **birebir aynı**; (f) `[` ile başlayıp bağlantı olmayan metin ("[2024] verisi") kayıpsız geçer.
4. `pnpm tsx --conditions=react-server scripts/eval/akis-denetim.ts` → mevcut 7 senaryo + yeni 2 senaryo geçer: `rehber-uydurma` (model `[OBP](/rehber/obp-nedir) ve [Yatay geçiş](/rehber/yatay-gecis-rehberi)` yazar; `rehberler` seçeneği gerçek `content/rehber` listesinden verilir; beklenti `baglantiDuzeltme === 2`, kaydedilen metinde `/rehber/obp-nedir-nasil-hesaplanir` ve `/rehber/yatay-gecis-cap-yandal` var, `/rehber/obp-nedir)` yok) ve `rehber-gercek` (gerçek slug, `baglantiDuzeltme === 0`). `Beklenen` tipi `baglantiDuzeltme` ile genişler; eski senaryolara `0` yazılır.
5. `typegen + tsc + lint` temiz.
### A-4 Commit planı
1. `feat(sohbet): rehber bağlantıları akışta gerçek slug'larla doğrulanıyor` — baglanti-denetimi.ts, sohbet-akisi.ts, route.ts, danisman-prompt.ts (BAĞLANTI cümlesi), baglanti-denetim.ts, akis-denetim.ts.
2. `feat(revizyon): revizyon sayısı sınırı kaldırıldı — tek koşul paket + sohbet kredisi` — kalan dosyalar.
3. `[Bilal]` ayrı commit **yok**; pazarlama metni değişikliği Bilal'in açık isteği.
### A-5 Uzman ve denetim
- `yapay-zeka-muhendisi`: prompt cümlesini ve yakın-eşleşme eşiklerini (0,5 / 0,15) gözden geçirir; `pnpm eval:sohbet`'e "rehberde olmayan konu" senaryosu ekler (backlog).
- `guvenlik-uyum`: merge öncesi A-2 (para/paket vaadi değişiyor; `/paket` metni ve koşullar sayfası tutarlı mı). Link düzeltmede log yalnız slug içerir, kişisel veri yok.
---
## Paket B — Revizyon "ilk N" garantisi (madde 4)
### B-0 Kök neden (doğrulandı)
Boru hattı var ama **belirleyici değil**:
- `efektifParams` (`revizyon-kurali.ts:45-83`) İstanbul'u havuz süzgecine ekliyor ✔; `havuzOlustur` `oncelikli`leri kırpmada öne alıyor (`rapor-havuzu.ts:343-347`) ✔; havuz JSON'unda `"oncelikli": true` işaretleniyor (`rapor.ts:424-442`) ✔; prompt "EN AZ N tane seç" diyor (`rapor.ts:566-571`) — **ricadır**.
- `secimiOnar` (`rapor.ts:207-270`) modelin seçtiği geçerli satırları olduğu gibi alır, yalnız **eksik** kalan yeri `uyum` (+4 öncelikli) sırasıyla doldurur. Model 24 geçerli satırı öncelikli satırsız doldurursa hiçbir şey tamamlanmaz; `ilkTercihleriOneAl` (`rapor.ts:866`) öne alacak satır bulamaz → "ilk 5 İstanbul" istendi, liste değişmedi gibi görünür. (`YETERLI_MODEL_SECIMI` = 12 olduğundan ikinci deneme de tetiklenmez.)
- Dev mock yolu (`rapor.ts:572-575` → `mockRapor` `:368-422`) `ilkTercihleriOneAl`/`oncelikli`yi hiç uygulamaz — lokalde anahtarsız denemede kural hiç görünmez.
- Model kuralı hiç çıkarmıyorsa (liste_revize'yi `iller.ekle` ile çağırıp `ilkTercihler` vermiyorsa) bu kod tarafı değil; `pnpm eval:sohbet` konusu (yapay-zeka-muhendisi, B-5).
### B-1 Değişiklik
**`revizyon-kurali.ts`** — yeni saf fonksiyon:
```ts
/**
* Dilim seçimi kurulduktan sonra "ilk N" ölçütüne uyan satır sayısı N'den
* azsa, havuzdaki uyan ama seçilmemiş programlar, aynı dilimdeki en düşük
* uyumlu uymayan satırların yerine konur (dilim adetleri değişmez). Havuzda
* yeterli uyan yoksa eldeki kadarı; takas edilen satır kaynak "tamamlandi"
* olur (gerekçe koddan şablonlanır). Dönen `eklenen` = takas sayısı.
*/
export function ilkTercihleriGarantile<S extends { programId: string; kaynak: string }>(
secim: Record<Dilim, S[]>,
havuz: readonly AdayProgram[],
k: RevizyonKurali | undefined,
uyum: (p: AdayProgram) => number,
yeniSatir: (p: AdayProgram) => S,
): { secim: Record<Dilim, S[]>; eklenen: number };
```
Takas kuralı: öncelikli adaylar `uyum` azalan sırayla; her aday kendi `dilim`inde, uymayan satırlar arasından `uyum`u en düşük olanı çıkarır (model gerekçesi olan satırlar önce korunur: `kaynak === "model"` olanlar en son çıkar). Dilim adetleri sabit kalır; dilim hedefi 0 olan dilimde takas yapılmaz. Havuz öncelikli-dilim dağılımı N'e yetmiyorsa eldeki kadar.
**`rapor.ts`**:
- `secimiOnar` sonrası, `nihai` belirlendikten sonra (`:703` civarı, metin denetiminden önce) `ilkTercihleriGarantile(nihai.secim, havuz, kural, uyum, (p) => ({ programId: p.id, gerekce: null, kaynak: "tamamlandi" }))` uygulanır; `onarim.tamamlanan += eklenen`; `tani`ye `ilkNEklenen` alanı eklenir (RaporTani tipi genişler).
- `mockRapor` (`:368-422`): `oncelikli` süzgecini alır, `dilimden` seçimini `oncelikli` olanları önce alacak şekilde sıralar ve sonunda `ilkTercihleriOneAl` uygular (mock prod sırasını taklit eder — kayıtlı yorum zaten "prod ile aynı hedef hesabı" diyor).
- Kullanıcı mesajındaki "EN AZ N tane seç" cümlesi kalır (modelin gerekçe yazması için), ama sonuç koda bağlıdır.
### B-2 Test — `scripts/eval/revizyon-denetim.ts` (Y)
`akis-denetim.ts` deseni: sahte OpenAI ucu; `AI_BASE_URL/AI_API_KEY` sahte; `TYPESAFE_API_KEY` silinir; `data/yokatlas.db` gerçek (salt okunur), app.db gerekmez. Sahte model `chat/completions` isteğinden `response_format.json_schema` adetlerini (`hayal/dengeli/guvenli` `minItems`) ve kullanıcı mesajındaki `ADAY HAVUZU … : {…}` JSON'unu okur, her dizi için **`oncelikli` olmayan** id'lerden tam adet seçer (gerekçe "test"), `genelDegerlendirme`/`uyarilar` sabit. Senaryolar:
1. `ilk5-istanbul`: params `{ sira: 100000, tur: "say", iller: ["Ankara","İzmir"], kategoriler: [], revizyon: { ilkTercihler: { adet: 5, iller: ["İstanbul"] }, not: "ilk 5 İstanbul" } }` → `raporUretAyrintili(params, "ilk 5 İstanbul", undefined)`; beklenti: `rapor.tercihler[0..4]` her birinin `programlar[id].il === "İSTANBUL"` (veri büyük harf; `olcuteUyar` zaten `tr-TR` küçültüyor), `tercihler.length === 24`, `tani.ilkNEklenen === 5`, dilim adetleri `kapsam.dagilim` ile aynı.
2. `ilk3-vakif`: `ilkTercihler: { adet: 3, universiteTipi: "vakif" }` → ilk 3 satır `uniturDevletMi(unitur) === false`.
3. `havuz-yetersiz`: `ilkTercihler: { adet: 10, iller: ["Ardahan"] }` → hata fırlatmaz, ilk satırlar eldeki Ardahan programları (0 da olabilir), sonuç 24 satır; `tani.ilkNEklenen ≤ 10`.
4. `mock-yolu`: `AI_API_KEY` boş + `NODE_ENV !== "production"` ile `mockAktif()` → senaryo 1 params; ilk 5 İstanbul.
Çıkış 0/1. Sahte ucun "kasten öncelikliyi atlaması" test koduna yorumla yazılır (gerçek model davranışını ölçmez).
### B-3 Kabul kriterleri
- `pnpm tsx --conditions=react-server scripts/eval/revizyon-denetim.ts` → 4/4, çıkış 0.
- `pnpm tsx --conditions=react-server scripts/eval/akis-denetim.ts` hâlâ geçer (B dokunmaz, regresyon kontrolü).
- `typegen + tsc + lint` temiz. `eval:robot` çağrı imzası değişmez (`raporUretAyrintili` dört parametre).
### B-4 Commit planı
1. `fix(revizyon): "ilk N" ölçütü seçimde kodla garanti ediliyor; mock yolu da kuralı uyguluyor` — revizyon-kurali.ts, rapor.ts.
2. `test(revizyon): mekanik ilk-N denetimi (sahte model önceliği atlar)` — revizyon-denetim.ts.
### B-5 Uzman
- `yapay-zeka-muhendisi`: `pnpm eval:sohbet`'e "ilk 5'i İstanbul'a göre güncelle" / "ilk 5'i X şehrine göre güncelle" cümleleriyle **araç argümanı** denetimi ekler (`ilkTercihler.adet === 5 && iller == ["İstanbul"]`); model `iller.ekle` ile yetiniyorsa `danisman-prompt.ts:85-89` örnekleri genişletilir (bu A'nın dosyası — bulgu rapora, değişiklik sonraki tura).
---
## Paket C — Program varyant etiketi (madde 2, 3)
### C-0 Kök neden (doğrulandı, veriden)
- `data/yokatlas.db`: `(universite, isim)` ikilisi **4.225** grupta tekrar ediyor (`SELECT universite, isim FROM programs GROUP BY 1,2 HAVING COUNT(*)>1`). Örnek BAŞKENT ÜNİVERSİTESİ / Tıp: 6 satır (id 200610657, 200610666, 200611488, 200690357, 200690358, 200690359), `isim` hepsinde "Tıp"; fark `program_ozellik`'te: `burs_orani` ∈ {Ücretli, %50 İndirimli, Burslu} × `ogrenim_dili` ∈ {Türkçe, İngilizce}. İÖ/UOLP'de de aynı (Marmara Hukuk: `107210332` "Hukuk" ve `107200444` "Hukuk (UOLP-…)(%25 İndirimli)" — UOLP parantezi isimde kalmış, burs/dil kalmamış).
- `program_ozellik` kapsamı tam: 21.493 programın 0'ı eksik.
- Hiçbir liste varyantı göstermiyor: `rapor-listesi.tsx:242-262`, `program-tablosu.tsx:352-367`, `liste-cekmecesi.tsx:438-455`, `universite-program-tablosu.tsx`, `bolum-program-listesi.tsx` (grep: `burs|ozellik` → 0). Danışman/liste modelinin havuz JSON'u da varyantı görmüyor (`rapor-havuzu.ts:427-452`).
- Birebir aynı kayıt (aynı id) tekrarı yok: `secimiOnar` tekrarları atıyor (`rapor.ts:233-236`). "Tekerrür" algısı yalnız etiket eksikliğinden.
### C-1 Tasarım — tek kaynak `Program.varyant`
**`src/lib/program-varyant.ts` (Y, saf):**
```ts
export type VaryantGirdisi = { bursOrani: string | null; ogrenimDili: string | null; ogrenimTuru: string | null };
/** Satır altı etiketi: "Burslu · İngilizce", "%50 İndirimli", "İkinci Öğretim", "Ücretli · Uzaktan"; Türkçe + Örgün + burs bilgisi yoksa null. */
export function varyantEtiketi(g: VaryantGirdisi): string | null;
```
Sıra: burs (varsa, olduğu gibi: "Burslu" / "%50 İndirimli" / "Ücretli"), dil (yalnız "Türkçe" değilse), öğretim türü (yalnız "Örgün Öğretim" değilse; metin DB'deki hâliyle: "İkinci Öğretim", "Açıköğretim", "Uzaktan Öğretim"). Ayraç " · ".
**`src/types/yokatlas.ts`:** `Program`'a `varyant: string | null` (zorunlu alan; `ProgramListeSatiri` `Omit` olduğu için otomatik taşır; `ManuelTercih` ayrı tip).
**`src/lib/db.ts`:** `SELECT_COLS` (`:40-42`) üç ilişkili alt sorgu kazanır: `(SELECT burs_orani FROM program_ozellik o WHERE o.id = programs.id) AS bursOrani`, aynı şekilde `ogrenimDili`, `ogrenimTuru`. Tüm `SELECT ${SELECT_COLS} FROM programs` kullanımları takma adsız (`db.ts:224,322`, `katalog.ts:199,519`, `araclar.ts:945,957`) → `programs.id` çözümlenir. `ProgramHamSatiri` üç alanı ekler; `programSatiri` (`:56`) `varyant: varyantEtiketi(row)` üretir. `programs_arsiv` sorguları (`ARSIV_SELECT`) dokunulmaz (arşivde ozellik yok; kapanan programlar için varyant null kalır). Tablo yokluğu: `ozellikTablosuVarMi` koruması alt sorgu için gerekmez — `data/yokatlas.db` depoda ve Docker seed'inde (`Dockerfile:28,75`) tabloyla geliyor; yazılımcı bunu commit mesajına yazar.
**Model ve danışman:** `rapor-havuzu.ts:427` `havuzuKompaktJson` satırına `...(p.varyant ? { varyant: p.varyant } : {})` eklenir (model aynı programın burslu/ücretli varyantlarını ayırt eder; gerekçede "burslu" yazabilir — `metindekiSayilar(p.isim, …)` sayı denetimi "%50" için `rapor.ts:779` listesine `p.varyant` eklenmesi gerekir; bu B'nin dosyası → **C raporuna not, entegrasyonda orkestratör tek satır ekler**: `...metindekiSayilar(p.isim, p.universite, p.varyant ?? "")`).
**UI (satır altı):** `liste-satiri.ts`'e `SATIR_VARYANT = "shrink-0 rounded-full bg-slate-100 px-2.5 py-0.5 text-xs font-semibold text-slate-700"` eklenmez — `SATIR_CIP` zaten bu (§4.15 tür çipi). Varyant **`SATIR_META` satırında `TurCipi`'nin hemen ardından `SATIR_CIP` sınıfıyla bir çip** olarak gösterilir (`rapor-listesi.tsx:264` sonrası, `program-tablosu.tsx` aynı yer, `universite-program-tablosu.tsx`, `bolum-program-listesi.tsx`). İnteraktif değil, `SectionEyebrow` kuralı dışında (§4.15 çip). `liste-cekmecesi.tsx:445-455` ikinci satırda üniversite adından sonra `· {tercih.varyant}` (`text-slate-500`). Null ise hiçbir şey çizilmez.
**Manuel liste:** `liste-dogrula.ts:21-33` `ManuelTercih.varyant?: string | null` (opsiyonel: eski localStorage kayıtları geçer; `dogrula` string ise alır); `use-tercih-profili.ts` `programEkleIste` kopyalarken `varyant` taşır; `program-ekle-butonu.tsx:22-31` `ProgramOzet` pick'ine `"varyant"` eklenir; `liste-queries.ts:15-29` `programOzetle` `varyant` döner.
**Kayıtlı rapor (`/listem`):** `rapor-queries.ts` `kullanicininRaporu` (`:35`) sonucu dönmeden önce `programOzellikleriGetir(Object.keys(result.programlar))` ile `result.programlar[id].varyant = varyantEtiketi(...)` yazılır (24 id, tek sorgu; eski kayıtlar da kazanır). `RaporSonuc.programlar[id]` tipine `varyant?: string | null` eklenir (`src/features/rapor/types/rapor.ts` — **C'ye ait**, B dokunmaz). `rapor-listesi.tsx` `p.varyant` okur. `listeOzetiCikar` (rapor.ts, B) bu turda varyantı modele yazmaz — backlog.
**Tekilleştirme kuralı ([KARAR]):** birebir aynı kayıt tekrarı yok; varyantlar ayrı ÖSYM kodu ve ayrı taban sırasıdır, listede ikisi de meşru. Ek kural **eklenmez**. Model varyantı görünce zaten "Başkent Tıp burslu + Başkent Tıp %50" seçimini gerekçelendirebilir. Üçten fazla varyantın aynı listeye girmesi sorun olursa `secimiOnar`'a dilim içi üst sınır (B'nin dosyası) sonraki tur.
### C-2 Test — `scripts/eval/varyant-denetim.ts` (Y)
`pnpm tsx scripts/eval/varyant-denetim.ts` (react-server koşulu gerekmez; `db.ts` ve `program-varyant.ts` saf/sqlite):
1. `varyantEtiketi` birim: `{Burslu, İngilizce, Örgün}` → "Burslu · İngilizce"; `{null, Türkçe, Örgün}` → null; `{Ücretli, Türkçe, İkinci Öğretim}` → "Ücretli · İkinci Öğretim"; `{%50 İndirimli, null, null}` → "%50 İndirimli".
2. `programGetir("200610657")…("200690359")` 6 Başkent Tıp kaydının `varyant` değerleri **birbirinden farklı** ve hiçbiri null.
3. `dilimAra(100000, "say", "dengeli", { limit: 50 })` sonucunda her satırda `varyant` alanı tanımlı (string ya da null) — SELECT_COLS'un bütün yolu kapsadığının kanıtı.
4. Devlet/Türkçe/örgün bir program (ör. `107210332` Marmara Hukuk) → `varyant === null`.
### C-3 Kabul kriterleri
- Test 4/4, çıkış 0; `typegen + tsc + lint` temiz; `pnpm tasarim:denetim` sıfır hata (yeni çip `SATIR_CIP` sınıf dizisiyle birebir).
- `grep -n "varyant" src/features/rapor/components/rapor-listesi.tsx src/features/liste/components/program-tablosu.tsx src/features/liste/components/liste-cekmecesi.tsx` her dosyada ≥1.
- `sqlite3 data/yokatlas.db` dosyası **değişmez** (`git status` temiz; test salt okur).
### C-4 Commit planı
1. `feat(veri): Program.varyant — burs/dil/öğretim türü etiketi tek kaynaktan (program_ozellik)` — types, db.ts, program-varyant.ts, rapor-havuzu.ts, varyant-denetim.ts.
2. `feat(liste): varyant çipi — sonuç tablosu, kendi listem çekmecesi, katalog tabloları` — liste-queries, liste-dogrula, use-tercih-profili, program-ekle-butonu, program-tablosu, liste-cekmecesi, universite-program-tablosu, bolum-program-listesi.
3. `feat(listem): kayıtlı raporun satırlarına varyant (okuma anında zenginleştirme)` — rapor-queries.ts, types/rapor.ts, rapor-listesi.tsx.
### C-5 Uzman
- `veri-muhendisi`: `scripts/veri-kalite.ts`'e "aynı (universite, isim, fakulte) grubunda varyant etiketi tekrar eden satır" denetimi (bu turda değil; backlog). UOLP/ÇAP parantezlerinin `isim`de kalmasının (`bolumBazAdi`) varyantla çakışıp çakışmadığını gözden geçirir.
- `tasarimci`: çipin satırdaki yeri ve 375 px'te sarma (DESIGN §4.15 "ad sarar, çipler altına iner").
---
## Paket D1 — Sohbet UI: shimmer, logo varyantı, soru geçişi (madde 5, 10)
### D1-0 Kök neden (doğrulandı)
- **Shimmer yok:** `soru_sor` aracı `HAPSIZ_ARACLAR`'da (`arac-pilleri.tsx:48`) → hap çizilmez; form yalnız `output-available` olunca gelir (`sohbet-client.tsx:112-119`). Sorular üretilirken (`tool-soru_sor` parçası `input-streaming`/`input-available`) metin boş olduğundan `CevapIskeleti` (gri üç çizgi, `:316-324`) görünür; `TextShimmerLoader` (`loader.tsx:322-350`) var ama kullanılmıyor.
- **Logo:** site header `AnimasyonluMark sadeceHover` = varsayılan varyant **`rota`** (`site-header.tsx:26`, `animasyonlu-mark.tsx:108,172`). Sohbet avatarı araç çalışırken **`zincir`** (`sohbet-client.tsx:305-307`). Bilal'in istediği: header'dakiyle aynı → `rota`.
- **Soru geçişi:** `SohbetAnketi` (`sohbet-anketi.tsx:85-142`) shadcn `Questionnaire` ilkeli; `Item` aktif değilken `hidden` + `inert` alır (node_modules `@shadcn/react/dist/questionnaire/index.js`, `Je()` → `itemProps.hidden: !_`), geçiş animasyonu yok. Sihirbaz (`sihirbaz-adimlar.tsx:252-282`) `TransitionPanel` (`src/components/core/transition-panel.tsx`) + yön (`yon`), `x: ±100%`, `opacity`, `height` (useMeasure), yay `stiffness 300 / damping 30`, hareketi azalt'ta çapraz solma.
### D1-1 Değişiklik
**Madde 5 (sohbet-client.tsx):**
- `DanismanAvatar`: `varyant={canli && aracta ? "rota" : "nabiz"}`; yorum güncellenir ("header logosuyla aynı rota koreografisi").
- Mesaj gövdesinde (`:1004-1017`): `const soruHazirlaniyor = m.parts.some(p => p.type === "tool-soru_sor" && "state" in p && p.state !== "output-available" && p.state !== "output-error")`. `metin` boş ve `soruHazirlaniyor` ise `CevapIskeleti` yerine `<TextShimmerLoader text="Soruları hazırlıyorum…" size="sm" className="px-0.5" />` (`role="status"` sarmalı, `aria-live="polite"`). Metin varsa shimmer satırı metnin altında, formun yerinde durur; form gelince kalkar.
- `TextShimmerLoader` gradyanı `--muted-foreground/--foreground` token'ı kullanıyor (`loader.tsx:340`). `pnpm tasarim:denetim` ya da görsel dil bunu reddederse **yalnız o zaman** `loader.tsx`'te gradyan `var(--color-slate-400)` → `var(--color-slate-900)` → `var(--color-slate-400)` olur (karanlık mod token'dan döner; `dark:` yok).
**Madde 10:**
- **`src/lib/adim-gecisi.ts` (Y, saf, "use client" yok):** sihirbazdaki varyant ve geçiş sabitleri buraya taşınır:
```ts
export function adimGecisiVaryantlari(opts: { hareketiAzalt: boolean; yukseklik: number | "auto" }): Variants; // enter/center/exit, custom = yön
export function adimGecisi(hareketiAzalt: boolean): Transition; // x/height spring 300/30, opacity 0.2; azaltılmışta 0.12 solma
```
`sihirbaz-adimlar.tsx:254-282` bu iki fonksiyonu kullanır; davranış birebir (diff yalnız taşınma).
- **`sohbet-anketi.tsx`:** `Questionnaire` kontrollü olur: `item={aktif}` + `onItemChange={(ad) => { setYon(indeks(ad) > indeks(aktif) ? 1 : -1); setAktif(ad); }}` (ilkel `item`/`onItemChange` prop'larını destekliyor, `index.d.ts:23-27`). Her `QuestionnaireItem` içeriği (`Title/Description/Choices/Error`) `motion.div`'e sarılır: `variants={adimGecisiVaryantlari(...)}`, `custom={yon}`, `initial={false}`, `animate={s.ad === aktif ? "center" : "enter"}`, `transition={adimGecisi(hareketiAzalt)}`; sarmal kap `relative overflow-hidden`. Yükseklik: aktif içerik `useMeasure` ile ölçülür, sihirbazdaki gibi `height` varyanta geçer (ilk ölçümde `"auto"`).
- **Çıkış animasyonu sınırı:** ilkel pasif `fieldset`'i anında `hidden` yaptığı için çıkış kayması görünmez. Yazılımcı önce `QuestionnaireItem`'a `hidden={false}` ve `inert={false}` geçip görünürlüğü `data-active` ile kendisi yönetmeyi dener — bunun işe yaraması `b` (mergeProps) fonksiyonunda kullanıcı prop'unun kazanmasına bağlı: `node_modules/.pnpm/@shadcn+react@0.3.1_*/node_modules/@shadcn/react/dist/chunk-HBS6WEDP.js` içindeki `b` okunur. Kazanıyorsa pasif item'lar `TransitionPanel` mantığıyla (`exit` → `position:absolute`) tam çift yönlü geçiş alır; kazanmıyorsa yalnız giriş animasyonu kalır ve rapora "koddan çıkarım: çıkış görünmüyor" yazılır. Her iki durumda form gönderimi (`FormData`, `:70-78`) bozulmamalı: pasif item'ların input'ları DOM'da kalır.
- Ortak bileşen kararı ([KARAR]): `TransitionPanel` **taşınmaz**; sihirbaz paneli çocukları değiştirirken anket ilkelin kendi item'larını yönetiyor. Ortak olan *sabitler ve varyant üreticisi* (`adim-gecisi.ts`); bu "birebir aynı geçiş"i sağlar, bileşeni zorlamaz.
### D1-2 Kabul kriterleri
- `grep -n '"zincir"' src/features/rapor/components/sohbet-client.tsx` → 0; `grep -n "TextShimmerLoader" src/features/rapor/components/sohbet-client.tsx` ≥ 1.
- `grep -n "stiffness: 300" src/features/sihirbaz/components/sihirbaz-adimlar.tsx src/features/rapor/components/sohbet-anketi.tsx` → 0 (sabit tek kaynakta: `src/lib/adim-gecisi.ts`'te 1).
- `sohbet-anketi.tsx` `item=` ve `onItemChange=` içerir; `sihirbaz-adimlar.tsx` `adimGecisiVaryantlari` import eder.
- `typegen + tsc + lint` temiz; `pnpm tasarim:denetim` sıfır hata.
- Rapor: çıkış animasyonu elde edildi mi (mergeProps bulgusu, dosya:satır).
### D1-3 Commit planı
1. `feat(sohbet): soru formu hazırlanırken shimmer; araçta avatar header logosuyla aynı rota koreografisi`.
2. `refactor(hareket): sihirbaz adım geçişi sabitleri lib/adim-gecisi.ts'e; soru formu aynı geçişle`.
### D1-4 Uzman
- `tasarimci`: shimmer metni ("Soruları hazırlıyorum…") ve DESIGN §4.20/§4.33'e eklenecek iki cümle (D1 raporuna yazılır; DESIGN.md D2'nin). Dev sunucusu açabilir (AGENTS.md istisnası) — 375/768'de soru geçişinde yükseklik sıçraması ve `overflow-hidden`'ın odak halkasını kırpması kontrol.
---
## Paket D2 — Program ayrıntı çekmecesi (madde 8, 9)
### D2-0 Kök neden (doğrulandı) ve itiraz
- Bilgi paneli `program-ayrinti.tsx` (`ProgramAyrintiButonu`, Info ikonu `:140-170`); `liste-cekmecesi.tsx`'te böyle bir panel yok. "Yıllara göre" = `YillaraGore` (`:284-336`), `AyrintiIcerik`'te `:221`.
- Açılış bug'ı: `AltSayfaIcerik duraklar={[0.6, 0.92]} ilkDurak={0}` (`:171-175`). (a) İlk durak 0,6'da sayfa "kısık"tır → `AltSayfaGovde` `overflow-hidden touch-none` (alt-sayfa.tsx:631), kaydırılamaz; kullanıcı dokunup kaydırmaya çalışır, "takılmış" görür. (b) Panel iskeletle (6×h-10 ≈ 280 px) açılır, veri gelince (yerel sqlite, ~100 ms; genelde açılış yayı 0,4 s bitmeden) içerik ~3-4 ekran olur → `ResizeObserver` (`alt-sayfa.tsx:303-327`) `boy`u günceller, `ofset(d) = boy − durakPx(d)` sıçrar; yay sürüyorsa `git(d)` yeniden hedefler, bitmişse `y.jump` → gözle görülür zıplama. "Çok uzun diye" doğru sezgi; asıl neden açılırken boyun değişmesi ve çift durak.
- Madde 9'daki "logo sonra içerik": `akademik-ayrinti.tsx:120-270` `AcilisSahnesi` — üç renkli kare logo olarak belirir, yerine büyür, içerik `data-sonra` bloklarıyla bulanıktan netleşir. Bilal aynısını program ayrıntısında istiyor. Sahne `[data-kare]` kartlara ihtiyaç duyuyor; program çekmecesi kartsız (§4.19) → sahneye "kartsız" mod gerekir.
### D2-1 Değişiklik
**`src/components/ui/acilis-sahnesi.tsx` (Y):** `AcilisSahnesi` `akademik-ayrinti.tsx`'ten buraya taşınır (sabitler `LOGO`, `KARE_*`, `ICERIK_BASI`, `GUCLU_CIKIS`, `YERLESME` dâhil) ve **`hazir?: boolean`** (varsayılan `true`; `false` iken sahne oynamaz, `true` olunca bir kez oynar — veri bekleyen çekmece için) ve **kartsız mod** kazanır: `[data-kare]` yoksa ve `[data-logo]` varsa, `data-logo` kabındaki üç kare (`data-kare` yerine `data-logo-kare="sol|sag-ust|sag-alt"`, mutlak konumlu, `LOGO` renk/konum) `BASLANGIC + slot·SLOT_ADIMI` gecikmeyle belirir (`GUCLU_CIKIS`), `KARE_SURE·0.52`'ye kadar durur, sonra `scale 0.6 + opacity 0` ile söner (toplam ≈ 1 s), `[data-sonra]` blokları `netlestir` ile `ICERIK_BASI`'ndan itibaren `0.08 s` arayla gelir. Hareketi azalt: yalnız 0,25 s solma (mevcut dal). `akademik-ayrinti.tsx` yeni modülü import eder; davranışı değişmez.
**`program-ayrinti.tsx`:**
- `YillaraGore` bileşeni ve `:221` çağrısı silinir (API `gecmis` alanı kalır; backlog). `AyrintiIcerik` içindeki bölümler `data-sonra` alır (Kunye `ul`, Kontenjan türleri bloğu, "Son yerleşenin netleri" başlık+not+tablo tek sarmalda, Kosullar `details`, Istihdam, UniversiteOzeti, kaynak paragrafı). Başlığın altında `<div data-logo className="relative mx-auto h-16 w-16" aria-hidden>` (üç `data-logo-kare` span) — yalnız `durum === "hazir"` iken render; `AcilisSahnesi hazir={durum === "hazir"}` sarmalı `:218` civarı.
- Çekmece: `duraklar` ve `ilkDurak` prop'ları kaldırılır (varsayılan tek durak 0,92; `AkademikAyrinti` ile aynı). Yükleme iskeleti `min-h-[70dvh]` sarmalda (açılışta boy sabit; veri gelince ResizeObserver sayfayı yalnız büyütür, konum sıçramaz — `alt-sayfa.tsx:11-12` "içerik daha kısaysa içeriğin boyunda kalır" kuralı korunur, iskelet kısa değildir).
- Veri geldiğinde içerik sahne bitene kadar görünmez (sahne `opacity:0` ile başlatıyor) — "logo sonra içerik" hissi buradan.
**`docs/tasarim/DESIGN.md` §4.19:** "sayfa ekranın %60'ında açılır … tam boya çıkar" cümlesi (program-ayrinti.tsx:171-173 yorumu ve §4.10:372 atfı) → tek durak; "Yıllara göre" maddesi listeden çıkar; açılış koreografisi "AcilisSahnesi kartsız mod (logo → içerik), `src/components/ui/acilis-sahnesi.tsx`" olarak bir cümleyle eklenir. §4.10:372'deki "(§4.19)" atfı güncellenir.
### D2-2 Kabul kriterleri
- `grep -n "YillaraGore\|Yıllara göre" src/features/katalog/components/program-ayrinti.tsx` → 0.
- `grep -n "duraklar=\|ilkDurak=" src/features/katalog/components/program-ayrinti.tsx` → 0.
- `grep -n "function AcilisSahnesi" src/features/katalog/components/akademik-ayrinti.tsx` → 0; `src/components/ui/acilis-sahnesi.tsx` export eder; `akademik-ayrinti.tsx` import eder.
- `grep -n "data-sonra" src/features/katalog/components/program-ayrinti.tsx` ≥ 5; `data-logo` ≥ 1.
- `typegen + tsc + lint` temiz; `pnpm tasarim:denetim` sıfır hata (kapatma çarpısı sınıf seti değişmez).
- Tarayıcı doğrulaması yazılımcıda yok; `tasarimci` D2-4.
### D2-3 Commit planı
1. `refactor(ui): AcilisSahnesi ortak bileşen (components/ui/acilis-sahnesi.tsx), kartsız logo→içerik modu`.
2. `fix(program-ayrinti): tek durak + sabit iskelet boyu (açılış sıçraması), "Yıllara göre" kaldırıldı, logo→içerik açılışı; DESIGN §4.19`.
### D2-4 Uzman
- `tasarimci` (merge öncesi, dev sunucusu açabilir): 375 ve 768 px'te çekmece açılışında sıçrama yok mu; sahne süresi (≈1 s) uzun mu; `prefers-reduced-motion`. Gerekçe ve ölçümleri raporuna yazar, sunucuyu kapatır.
---
## Madde 7 — Rybbit tracking API: iş yok
`src/lib/analitik-sunucu.ts:15-17,33-52` `RYBBIT_HOST/SITE_ID/API_KEY` varsa `POST /api/track` atıyor; bayrak/TODO yok. İstemci tarafı (`analitik.ts`) `window.rybbit` script'i, tracking API ile ilgisiz. Env adları `.env.local`'da mevcut (değer okunmadı), CI `.gitea/workflows/ci.yaml:41-43` build'e geçiriyor, prod `docker-compose.yml` `.env.production` dosyasından alıyor. API sunucuda yeni açıldıysa `odeme_tamamlandi` ve `giris_basarili` (`:19`) olayları bugünden itibaren düşmeye başlamalı — doğrulama `analitik` ajanının işi (backlog).
---
## İtirazlar
1. **Madde 8-9 dosya adı.** "Liste çekmecesi" değil `program-ayrinti.tsx`; `liste-cekmecesi.tsx`'e yalnız C (varyant satırı) dokunur. Orkestratörün "sohbet-client.tsx ve liste-cekmecesi.tsx'e çakışmadan bölün" kısıtı bu sayede kendiliğinden sağlanıyor.
2. **Madde 8 çözümü "Yıllara göre"yi kaldırmakla bitmez.** Bug tablo uzunluğundan değil çift durak + açılışta boy değişiminden; tablo kalksa da panel ~3 ekran. D2 üçünü birlikte yapar.
3. **Madde 1 için "cikti-denetimi.ts'e ekle" yetmez.** Oradaki denetimler cevap bittikten sonra çalışır ve ekrandaki hapı geri alamaz; akış içi tampon gerekir (A-1). `cikti-denetimi.ts` dokunulmaz.
4. **Madde 2-3 için "rapor-havuzu/rapor.ts'te tekilleştir" yanlış yön.** Kayıtlar birebir aynı değil; sorun etikettir ve veri katmanında çözülür (`Program.varyant`), böylece /sonuc, katalog, kendi listem ve rapor aynı anda düzelir.
5. **Madde 6'da DB göçü gerekmez.** `revision_count` sayaç olarak kalır; kaldırmak iki göç ve geri dönüş maliyeti getirir, hiçbir kazanç yok.
6. **Madde 4 "test iste" — modelin kuralı çıkarıp çıkarmadığı mekanik testle ölçülemez;** B'nin testi kod yolunu kilitler, model tarafı `eval:sohbet`'e (yapay-zeka-muhendisi) gider.
## Merge sırası ve bağımlılıklar
A, B, C, D1, D2 birbirinden bağımsız; önerilen sıra **B → C → A → D2 → D1** (küçükten büyüğe, UI en sona). Entegrasyonda orkestratör: (1) `sohbet-client.tsx` prop yeniden adlandırması (çakışma kararı 1); (2) `rapor.ts:779` `metindekiSayilar(... p.varyant)` tek satırı (C-1 notu); (3) `pnpm build`; (4) `scripts/eval/{akis,baglanti,revizyon,varyant}-denetim.ts` dördü; (5) `pnpm eval:robot --profil … ` isteğe bağlı (anahtar ister, Bilal).
## Backlog önerileri (kapsam dışı)
1. `/universite/…` ve `/bolum/…` sohbet bağlantılarının da gerçek slug'larla doğrulanması (katalog haritaları `katalog.ts`); A'nın tamponu genişletilebilir.
2. `listeOzetiCikar` ve `program_ara` çıktısına `varyant` (danışman "burslu mu?" sorusuna listeden cevap versin).
3. `veri-kalite.ts`: aynı (üniversite, isim, fakülte) grubunda varyant etiketi tekrarlayan satır uyarısı; UOLP/ÇAP parantezleri.
4. `api/program-ayrinti` `gecmis` alanı artık kullanılmıyor → `programGecmisiGetir` sorgusu kaldırılabilir (küçük tasarruf).
5. `eval:sohbet`'e "rehberde olmayan konu" ve "ilk 5'i X'e göre güncelle" senaryoları (yapay-zeka-muhendisi).
6. `analitik`: tracking API açıldıktan sonra `odeme_tamamlandi` / `giris_basarili` olaylarının Rybbit'te göründüğünün teyidi; önceki dönem için huni boşluğu notu.
7. `sohbet-client.tsx` 1.105 satır; soru formu/revizyon parçası ayrı dosyaya (D1 sonrası).
8. Paketsiz ama kredili adayın `liste_revize` kullanıp kullanamayacağı — bugün paket şart (`liste-revizyon.ts:71-77`); Bilal'in "kredisi olduğu sürece" sözü bunu kapsıyorsa ürün kararı.
## Bilal'den istenen
1. Madde 6: paketsiz-kredili adayda revizyon açılsın mı, yoksa paket şart kalsın mı? (tek cümle, 1 dk) — şartname "paket şart kalır" varsayıyor.
2. Prod `.env.production` içinde `RYBBIT_HOST / RYBBIT_SITE_ID / RYBBIT_API_KEY` tanımlı mı (yalnız varlık; madde 7'nin sunucu olayları buna bağlı) — 1 dk.
3. D2 sahnesi için tercih: logo kareleri sönerek mi, yoksa başlığın yanına küçülerek mi kaybolsun? (varsayılan: sönerek) — 1 dk.