# 26 — Veri mühendisi: `programs.il` temizliği (A4/T34) + vakıf burs etiketi (A5) **Tarih:** 22 Eylül 2026, gece vardiyası · **Rol:** `veri-muhendisi` · **Mod:** yalnız teşhis. **Yazılan tek dosya:** bu rapor. `data/` altına, veritabanına, `scripts/` altına **hiç yazılmadı**; tüm bağlantılar `sqlite3 -readonly data/yokatlas.db`. Kopya alınmadı (disk), `pnpm install`/`build` koşulmadı. Kanıt işaretleri: **[D]** bu gece koşulan sorgu · **[K]** koddan çıkarım (dosya:satır) · **[doğrulanmalı]** birincil kaynakla teyit şart. --- ## 1. Beş maddelik özet 1. **Kök neden tek cümle: `il`, `fakulte` ve `isim` alanları yalnız INSERT yolunda yazılıyor, UPDATE yolunda hiç tazelenmiyor.** `scripts/refresh.ts:665-690` — mevcut satıra yalnız `sira/puan/kontenjan/yerlesen` yazılır. Bu yüzden 2021'de üçüncü parti bir CSV'den (`scripts/ingest.ts:4` → `yokatlas-dataset-2025`) gelen konum ve ad değerleri **kalıcı**: API doğrusunu söylese bile DB'ye giremiyor. `il` kirliliği bir "YÖK Atlas hatası" değil, **bizim tazeleme hattımızın kör noktası**. 2. **A5'in cevabı: kırılım YAPILABİLİR, veri elimizde.** Burs/dil etiketi YÖK Atlas API'sinde ayrı bir kolon değil, `birimAdi` alanının **içinde** geliyor ("Tıp (İngilizce) (Burslu)") — `scripts/refresh.ts:37-39` bunu açıkça yazıyor. Kanıt: 2026'da **yeni açılan** vakıf lisans satırlarının %93,7'si (326/348) etiketli, **eskiden beri var olan** satırların yalnız %14,2'si (628/4.427) etiketli [D]. Fark, `--isim-esitle` bayrağının varsayılan KAPALI olmasından geliyor. Tek koşuyla geri alınabilir. → **T11 "kırılım var" tarafına düşer.** 3. **A4'ün rakamları: 21.493 satırın 20.092'si tam ve temiz 81 Türkiye ili, 1.291'i KKTC/yurt dışı, 110'u boş** [D]. Türkiye tarafında eksik il yok, uydurma il yok — yani `/il/[slug]` (T7) yalnız bir **filtreye** ihtiyaç duyuyor, şema değişikliğine değil. 4. **Yanlış `unitur` 100 satır** (Kıbrıs Amerikan 42 + Uluslararası Balkan 34 + Uluslararası Saraybosna 24) [D] — bunlar sihirbazın "Devlet" adımında devlet programı gibi görünüyor. Ters yönde daha büyük bir sızıntı var ve şimdiye dek raporlanmamış: sihirbazın **"Vakıf (burslu olursa)"** kovası `unitur <> 'DEVLET'` ile kurulduğu için (`src/lib/db.ts:77-83`) içine **1.256 KKTC/yurt dışı satırı** giriyor — vakıf havuzunun %13,4'ü. 5. **Kapsam dışı ama yayını etkileyen bulgu:** `kontenjan` kolonu yalnız **genel kontenjanı** tutuyor (`kontenjan2025 = gk2025`, 19.857/19.857 satır), `yerlesen` ise **tüm kota türlerinin toplamı** (`gkY+sgyY+dprmY+obkY`, `scripts/refresh.ts:212-217`). Bu yüzden devlet lisans doluluğu **%102,1** çıkıyor [D] — matematiksel olarak imkânsız. Bu gece yayımlanan `bilgisayar-mi-yazilim-muhendisligi-mi` yazısındaki "devlet YM %102,1" rakamı bu sapmadır. **KKTC ve vakıf rakamları etkilenmiyor** (KKTC'de aşan satır 0; devlet lisansta 5.618) — yani `kktc-universiteleri-okunur-mu` yazısı güvende. --- ## 2. Veri kalitesi tablosu | # | Kontrol | Sorgu (hepsi `sqlite3 -readonly data/yokatlas.db`) | Sonuç | Önem | |---|---|---|---|---| | K1 | Farklı `il` değeri sayısı | `SELECT COUNT(DISTINCT il) FROM programs` | **91** (81 TR ili + 6 KKTC + 4 yurt dışı) | bilgi | | K2 | Boş `il` | `SELECT COUNT(*) FROM programs WHERE il IS NULL` | **110** satır / **9** kurum; hepsi `son_kilavuz_yili=2026` | **P1** | | K3 | TR dışı `il` satırı | `WHERE il IN ('LEFKOŞA','GİRNE','GAZİMAĞUSA','LEFKE','GÜZELYURT','KIBRIS','SARAYBOSNA - BOSNA - HERSEK','BİŞKEK-KIRGIZİSTAN','TÜRKİSTAN-KAZAKİSTAN','BAKÜ-AZERBAYCAN')` | **1.291** | **P1** | | K4 | TR ili tam mı | aynı listenin `NOT IN`'i + `COUNT(DISTINCT il)` | **81/81** — eksik il yok, fazla il yok | ✓ temiz | | K5 | Yanlış `unitur='DEVLET'` | `WHERE unitur='DEVLET' AND (universite LIKE 'KIBRIS AMERİKAN%' OR LIKE '%BALKAN%' OR LIKE '%SARAYBOSNA%')` | **100** satır | **P1** | | K6 | "Vakıf" kovasının saflığı | `WHERE unitur IS NOT NULL AND unitur<>'DEVLET' GROUP BY unitur` | VAKIF 7.974 · **KKTC 1.145** · VAKIF MYO 121 · **YURTDISI KAMU 99** · **YURTDISI VAKIF 12** | **P1** | | K7 | `unitur` boş | `WHERE unitur IS NULL` | **0** ✓ (`unitur-onar.ts` işini yapmış) | ✓ | | K8 | Aynı kurumun iki `universite` yazımı | `bazAd` çakışma sorgusu (§3.4) | **95 kurum / 1.449 satır** — bildirilen "10 KKTC üniversitesi" değil, **katalogun tamamı** | P2 | | K9 | Aynı kurumda iki farklı `unitur` | K8 sorgusunun `unitur` kırılımı | **4 kurum**: Balkan, Saraybosna, Manas, Yesevi (DEVLET↔YURTDISI KAMU / KAMU↔VAKIF) | **P1** | | K10 | Ayırt edilemeyen program adı (2026 aktif) | `GROUP BY universite,isim,tur,onlisans HAVING COUNT(*)>1` | **4.206 grup / 10.164 satır** (Vakıf 6.079 · Devlet 3.209 · KKTC 985) | **P1** | | K11 | Vakıf lisansta burs etiketi | §4 sorgusu | **3.821/4.775 etiketsiz** (KKTC 747/849) — bildirilen rakam **doğrulandı** | **P1** | | K12 | Etiketin satır yaşıyla ilişkisi | §4 sorgusu | 2026'da yeni: **326/348 etiketli**; eskiden var olan: **628/4.427** | **kanıt** | | K13 | `yerlesen > kontenjan` | `WHERE yerlesen2026 > kontenjan2026` | **11.964/21.493** satır; devlet lisansta 5.618, KKTC lisansta **0** | **P1** | | K14 | `kontenjan` = genel kontenjan mı | `kontenjan2025 <> gk2025` | **0** satır → evet, yalnız genel kontenjan | **kanıt** | | K15 | `netler` yetim satırı | `LEFT JOIN programs ... WHERE p.id IS NULL` | **8.215** satır (silinmiş/kimliği devredilmiş kodlara ait) | P2 | | K16 | `netler` yıl kapsamı | `SELECT MAX(yil) FROM netler` | **2025** — T36 doğrulandı, 2026 net verisi yok | **P1** | | K17 | Aykırı sıralama | `WHERE sira2026<=0` | **0** ✓ | ✓ | | K18 | `fakulte` boş (2026 aktif) | `WHERE fakulte IS NULL AND kontenjan2026 IS NOT NULL` | **0** ✓ | ✓ | --- ## 3. Görev 1 — `programs.il` (A4 / T34) ### 3.1 Bildirilen bulguların tek tek doğrulanması | Bildirilen | Durum | Kanıt | |---|---|---| | 91 farklı `il` değeri | **DOĞRU** | K1 | | `il` boş 110 satır / 9 üniversite | **DOĞRU** | K2; kurum listesi §3.2 | | Uluslararası Balkan Ü. 26 satır `il='SARAYBOSNA - BOSNA - HERSEK'`, kurum Üsküp'te | **DOĞRU** (26 satır) — ayrıca aynı kurumun 8 satırı `il` boş, 14 satırı ayrı adla `YURTDISI KAMU` | §3.2 | | Kıbrıs Amerikan Ü. 42 satır `unitur='DEVLET'` | **DOĞRU** (36 + 6, iki ad hattı) | K5 | | `src/lib/db.ts:79` devlet filtresi bunları devlet sayıyor | **DOĞRU** | `uniturFiltreSql`, `src/lib/db.ts:77-83` [K] | | 10 KKTC üniversitesi iki ayrı `universite` adıyla duruyor | **DOĞRU ama eksik tarif** — aynı desen **95 kurumda** var (1.449 satır). KKTC'ye özgü değil, API'nin 2026 ad biçiminden geliyor | K8 | | ODTÜ KKK programları ana kampüsle **aynı `universite` ve `il`** altında | **YARI ÇÜRÜTÜLDÜ.** 8 KKK satırı `il='KIBRIS'` ve `fakulte='ODTÜ Kuzey Kıbrıs Kampusu (KKTC-Güzelyurt)'` taşıyor — yani ayrılabilir. Ortak olan yalnız `universite`. **Ama** ODTÜ *Yazılım Mühendisliği*'nin 2 satırı gerçekten `il='ANKARA'`, `fakulte='Mühendislik Fakültesi'` ile duruyor → yazarın asıl iddiası doğru, sebebi farklı | §3.3 | ### 3.2 Boş `il`: 110 satır, 9 kurum, hepsi 2026 kılavuzundan ```sql SELECT universite, unitur, COUNT(*) FROM programs WHERE il IS NULL GROUP BY 1,2 ORDER BY 3 DESC; ``` | Kurum | `unitur` | Satır | `il` nereden türetilebilir | |---|---|---|---| | TİRAN NEW YORK ÜNİVERSİTESİ (TİRAN-ARNAVUTLUK) | YURTDISI KAMU | 36 | ad parantezi | | İÇİŞLERİ BAKANLIĞI VE MİLLİ SAVUNMA BAKANLIĞI ADINA SAĞLIK BİLİMLERİ ÜNİVERSİTESİNDE EĞİTİM ALACAKLAR | DEVLET | 15 | **türetilemez** (sahte kurum adı; bakanlık kontenjanı) | | ULUSLARARASI BALKAN ÜNİVERSİTESİ (ÜSKÜP-MAKEDONYA) | YURTDISI KAMU | 14 | ad parantezi | | ULUSLARARASI SARAYBOSNA ÜNİVERSİTESİ (SARAYBOSNA-BOSNA-HERSEK) | YURTDISI KAMU | 14 | ad parantezi | | ULUSLARARASI SARAYBOSNA ÜNİVERSİTESİ | DEVLET | 9 | kardeş satırdan | | ULUSLARARASI BALKAN ÜNİVERSİTESİ | DEVLET | 8 | kardeş satırdan (**Üsküp**, Saraybosna değil) | | KIRGIZİSTAN-TÜRKİYE MANAS ÜNİVERSİTESİ (BİŞKEK-KIRGIZİSTAN) | YURTDISI VAKIF | 8 | ad parantezi | | HOCA AHMET YESEVİ … (TÜRKİSTAN-KAZAKİSTAN) | YURTDISI VAKIF | 4 | ad parantezi | | KIBRIS AMERİKAN ÜNİVERSİTESİ | DEVLET | 2 | kardeş satırdan (LEFKOŞA) | **95 satır ad/kardeş satırdan doldurulabilir; 15 satır (MSB/İçişleri) doldurulamaz** — ÖSYM bunları tek bir yerleşkeye bağlamıyor. Tahminle doldurmuyoruz; eksik, eksik kalır ve `veri-kalite`'de **beyaz listeye** alınır. ### 3.3 Kök neden — hangi betik hangi alanı nereden yazıyor | Alan | Yazan | Kaynak | UPDATE yolunda tazeleniyor mu | |---|---|---|---| | `il` | `ingest.ts` (2021 CSV) → sonra yalnız `refresh.ts` INSERT (`r.ilAdi`, satır 679) | üçüncü parti CSV, sonra API | **HAYIR** | | `unitur` | `ingest.ts` CSV → `refresh.ts` INSERT `uniturSec()` (518-524) → `unitur-onar.ts` yalnız NULL'lar | CSV, sonra API (koşullu) | **HAYIR** | | `fakulte` | `ingest.ts` CSV → `refresh.ts` INSERT `fakulteSec()` | CSV, sonra API | **HAYIR** | | `isim` | `ingest.ts` CSV → `refresh.ts` INSERT `r.birimAdi`; UPDATE yalnız `--isim-esitle` ile (643-648) | CSV, sonra API | **bayrak kapalıyken HAYIR** | | `sira/puan/kontenjan/yerlesen` | `refresh.ts` UPDATE (474-480) | API | evet | Üç mekanizma üst üste biniyor: **(a) UPDATE körlüğü.** `refresh.ts:665-680` — `if (exists.get(id)) update.run(...)` yalnız dört yıl kolonunu yazar. 2021 CSV'sinden gelen `il`/`fakulte`/`isim` beş yıldır hiç tazelenmedi. Kıbrıs Amerikan'ın 36 satırı `il='LEFKOŞA'`, Balkan'ın 26 satırı `il='SARAYBOSNA - BOSNA - HERSEK'` — ikisi de **2021 verisi taşıyor** (`kontenjan2021 IS NOT NULL`), yani CSV kökenli [D]. **(b) `uniturSec` yanlışı kalıcılaştırıyor.** `refresh.ts:519-525`: ```ts const uniturSec = (universite: string, api: string | null): string | null => { for (const k of [universite, bazAd(universite)]) { const set = uniturlar.get(k); if (set?.size === 1) return [...set][0]; // ← DB'deki değer API'yi EZİYOR } return api; }; ``` Niyet doğru (sözlük tutarlılığı, `uniturOnar`'ın çelişki koruması patlamasın diye), sonuç ters: API "KKTC" dese bile DB'de tek bir `DEVLET` varsa `DEVLET` yazılır. Kıbrıs Amerikan'ın 2026'da eklenen 6 yeni satırı bu yüzden de `DEVLET` oldu. **(c) İki ayrı ad hattı = iki ayrı kaynak.** API 2026'da üniversite adını **`AD (İL)`** biçiminde veriyor (`ORTA DOĞU TEKNİK ÜNİVERSİTESİ (ANKARA)`, `DOĞU AKDENİZ ÜNİVERSİTESİ (KKTC-GAZİMAĞUSA)`). CSV ise sade ad + ayrı `il` veriyordu. Böylece 95 kurumda iki paralel ad hattı oluştu: ```sql -- K8 WITH b AS (SELECT universite, unitur, il, TRIM(CASE WHEN universite LIKE '%)' THEN substr(universite,1,instr(universite,' (')-1) ELSE universite END) baz, COUNT(*) n FROM programs GROUP BY 1,2,3) SELECT baz, universite, unitur, COALESCE(il,'(NULL)'), n FROM b WHERE baz IN (SELECT baz FROM b GROUP BY baz HAVING COUNT(DISTINCT universite)>1); -- 95 kurum / 1.449 satır ``` Yıl profili: `AD (TR ili)` eki 1.224 satır/80 kurum · `AD (KKTC-şehir)` 149/10 · `AD (şehir-ülke)` 76/5 [D]. **ODTÜ örneği — sorunun tam resmi.** `universite='ORTA DOĞU TEKNİK ÜNİVERSİTESİ (ANKARA)'` altında 11 satır var: 3'ü `il='ANKARA'` (KKTC uyruklu kontenjanı, doğru), 8'i `il='KIBRIS'` + `fakulte='ODTÜ Kuzey Kıbrıs Kampusu (KKTC-Güzelyurt)'` (doğru). **Ama** `universite='ORTA DOĞU TEKNİK ÜNİVERSİTESİ'` altındaki 72 eski CSV satırının içinde *Yazılım Mühendisliği* de var: ```sql SELECT universite, il, fakulte, isim, kontenjan2026, sira2026 FROM programs WHERE isim LIKE 'Yazılım Müh%' AND universite LIKE 'ORTA DOĞU%'; -- ORTA DOĞU TEKNİK ÜNİVERSİTESİ | ANKARA | Mühendislik Fakültesi | Yazılım Mühendisliği | 45 | 299241 -- ORTA DOĞU TEKNİK ÜNİVERSİTESİ | ANKARA | Mühendislik Fakültesi | Yazılım Mühendisliği | 5 | 293024 ``` ODTÜ Ankara'da Yazılım Mühendisliği **yoktur**; program Kuzey Kıbrıs Kampusu'ndadır (`ncc.metu.edu.tr/tr/sng`, yazar 13 teyit etmiş) [doğrulanmalı: 2026 ÖSYM kılavuzu]. 45/5 kontenjan ikilisi de KKK'nin klasik **Burslu / %50 İndirimli** desenidir — ama iki satırın adı birebir aynı, çünkü A5'teki etiket kaybı tam burada vuruyor. Yani üç hata aynı satırda: yanlış `il`, yanlış `fakulte`, kayıp burs etiketi. Aynı desen ODTÜ'de 16 ad grubunda daha var (Bilgisayar Müh. ×3 → 105/65/65, Elektrik-Elektronik ×3, İşletme ×6 …) [D]. ### 3.4 Etki haritası — hangi yüzey ne yanlış gösteriyor | Yüzey | Dosya | Ne oluyor | Etkilenen | |---|---|---|---| | **Sihirbaz "Devlet" adımı** | `src/lib/db.ts:78` `unitur = 'DEVLET'` | KKTC ve Balkan/Saraybosna programları devlet üniversitesi gibi havuza giriyor | **100 satır** | | **Sihirbaz "Vakıf (burslu olursa)" adımı** | `src/lib/db.ts:79-80` `unitur <> 'DEVLET'` | KKTC (1.145) + yurt dışı (111) satırı vakıf havuzuna giriyor; etiket kaybı yüzünden öğrenci hangisinin burslu olduğunu da göremiyor | **1.256 satır / havuzun %13,4'ü** | | **Sihirbaz il adımı (facet)** | `src/lib/db.ts:325-327` — kodda not düşülmüş: "81 il + KKTC/yurtdışı" | DAÜ'nün 276 satırı **GAZİMAĞUSA** ve **KIBRIS** çiplerine bölünmüş; aynı bölünme 9 KKTC kurumunda var | **1.080 satır** | | **`/universiteler` hub'ı** | `src/lib/katalog.ts:270-337` | `MAX(unitur)` + alfabetik ilk ad hattı kazanıyor → Uluslararası Balkan Ü. **"Devlet / Saraybosna"** görünüyor (doğrusu: yurt dışı kamu / Üsküp) | 3 kurum kartı | | **`/universite/`** | `src/lib/katalog.ts:402` `stats.devletMi` | Kıbrıs Amerikan Ü. sayfasında "devlet üniversitesi" rozeti | 1 sayfa | | **il faceti / il listesi** | aynı | `il` boş 110 satır **hiçbir il filtresine düşmüyor** — sessizce kayboluyor | 110 satır | | **`/il/[slug]` planı (T7, #18 yazısı)** | henüz yok | Bugünkü `SELECT DISTINCT il` 91 değer döner → `/il/baku-azerbaycan`, `/il/kibris` gibi sayfalar üretilir | **önkoşul** | | **Bu gecenin yazıları** | `content/rehber/bilgisayar-mi-yazilim-muhendisligi-mi.md` | ODTÜ head-to-head tablodan çıkarılmış ve **neden çıkarıldığı yazıda yazılmış** → doğru davranış, düzeltme gerekmiyor. Ama aynı yazının **devlet doluluk** rakamları §5'teki kota asimetrisinden sapmalı | 1 yazı, 2 rakam | | | `content/rehber/kktc-universiteleri-okunur-mu.md` | KKTC evreni `unitur='KKTC' OR universite LIKE 'KIBRIS AMERİKAN%'` ile elle kurulmuş → yazı **doğru**; düzeltme sonrası sorgu sadeleşir, rakam değişmez | — | ### 3.5 Önerilen şema — **yeni kolon yok** Soru "il yanında `ulke`/`bolge` mi, yoksa ayrı `kampus` alanı mı" şeklinde soruldu. **Cevabım: ikisi de değil.** Her iki bilgi de zaten DB'de: - **Ülke/statü ayrımı `unitur`'de yaşıyor:** `DEVLET · VAKIF · VAKIF MYO · KKTC · YURTDISI KAMU · YURTDISI VAKIF`. Bir `ulke` kolonu bu sözlüğün kopyasını çıkarır ve iki alanın çelişme ihtimalini yaratır. - **Kampüs bilgisi `fakulte`'de yaşıyor:** `ODTÜ Kuzey Kıbrıs Kampusu (KKTC-Güzelyurt)`, `ASBÜ-Kuzey Kıbrıs Yerleşkesi (KKTC-Lefkoşa)`, `İTÜ-KKTC EĞİTİM ARAŞTIRMA YERLEŞKESİ (GAZİMAĞUSA)` [D]. | | A) `ulke`/`bolge` kolonu | B) `il` normalize + `kampus` kolonu | **C) Kolon yok: `il`'i normalize et, `unitur`'ü düzelt** | |---|---|---|---| | Göç | `yokatlas-goc.ts` + `ingest.ts` + `refresh.ts` INSERT + `veri-kalite.ts` | aynısı | **yok** | | Dolduran kaynak | API'de **böyle bir alan yok** — ad parantezinden türetmek zorundayız | aynı sorun | ad parantezi (aynı iş, kolonsuz) | | Yıllık refresh maliyeti | her yıl yeni kolonun da senkronu | aynı + `kampus` eşlemesi | `il` senkronu (zaten yapılmalı) | | Okuyan taraf değişikliği | `katalog.ts`, `db.ts`, facet, tipler | aynısı + tipler | **tek satır filtre**: `AND unitur IN ('DEVLET','VAKIF','VAKIF MYO')` | | T7 (`/il/[slug]`) çözülür mü | evet | evet | **evet** | | Çift kaynak riski | `unitur` ile çelişebilir | `fakulte` ile çelişebilir | yok | **Öneri C.** İki `UPDATE` + bir satır kod. Kurucunun "over-engineering yapma" talimatına uyan en küçük çözüm. C'nin tek gerçek açığı, şema değil **hat** açığı: `il`/`fakulte`/`isim` UPDATE yolunda tazelenmediği sürece aynı kir her yıl birikir. Bunun kolonla ilgisi yok — çözümü `refresh.ts`'e `--kimlik-onar`/`--isim-esitle` ile aynı güvenlik desenini taşıyan bir **`--yer-esitle`** bayrağı: API ile `il`/`fakulte` farkı olan satırları önce **listeler**, gözle onaylanınca yazar. Ayrı bir CTO şartnamesine değer; bu gece yazılmaz. ### 3.6 Düzeltme betiği taslağı — **KOŞULMADI, yalnız yazıldı** Ön koşullar: dev sunucu **kapalı** (WAL tek yazar), önce yedek, sonra `pnpm veri-kalite --yil 2026`. ```sh # 0) Yedek — geri alma yolu budur. WAL'i önce kapat. sqlite3 data/yokatlas.db "PRAGMA wal_checkpoint(TRUNCATE);" cp data/yokatlas.db data/yokatlas.db.yedek-2026-09-22 ``` ```sql BEGIN; -- ── 1) unitur: yanlış DEVLET etiketleri (K5 = 100 satır) ──────────────── -- Kıbrıs Amerikan Ü. YÖK'ün KKTC listesinde (yok.gov.tr/tr/university?type=6) UPDATE programs SET unitur='KKTC' WHERE unitur='DEVLET' AND universite LIKE 'KIBRIS AMERİKAN%'; -- 42 -- Balkan + Saraybosna: aynı kurumun diğer ad hattı zaten YURTDISI KAMU UPDATE programs SET unitur='YURTDISI KAMU' WHERE unitur='DEVLET' AND (universite LIKE 'ULUSLARARASI BALKAN%' OR universite LIKE 'ULUSLARARASI SARAYBOSNA%'); -- 58 -- ── 2) il: KKTC'de 'KIBRIS' toplayıcısını şehre indir (179 satır) ─────── -- Şehir zaten ad ya da fakülte parantezinde yazılı; tahmin YOK. UPDATE programs SET il = CASE WHEN universite LIKE '%(KKTC-LEFKOŞA)%' THEN 'LEFKOŞA' WHEN universite LIKE '%(KKTC-GİRNE)%' THEN 'GİRNE' WHEN universite LIKE '%(KKTC-GAZİMAĞUSA)%' THEN 'GAZİMAĞUSA' WHEN universite LIKE '%(KKTC-LEFKE)%' THEN 'LEFKE' WHEN fakulte LIKE '%(KKTC-Lefkoşa)%' THEN 'LEFKOŞA' WHEN fakulte LIKE '%(KKTC-Güzelyurt)%' THEN 'GÜZELYURT' WHEN fakulte LIKE '%(GAZİMAĞUSA)%' THEN 'GAZİMAĞUSA' END WHERE il='KIBRIS' AND (universite LIKE '%(KKTC-%' OR fakulte LIKE '%(KKTC-%' OR fakulte LIKE '%(GAZİMAĞUSA)%'); -- beklenen: 179; sonrasında SELECT COUNT(*) FROM programs WHERE il='KIBRIS' = 0 -- ── 3) il: yurt dışı değerini mevcut sözleşmeye uydur (26 satır) ──────── -- Mevcut sözleşme: 'ŞEHİR-ÜLKE' (BİŞKEK-KIRGIZİSTAN, TÜRKİSTAN-KAZAKİSTAN) UPDATE programs SET il='ÜSKÜP-KUZEY MAKEDONYA' WHERE universite='ULUSLARARASI BALKAN ÜNİVERSİTESİ' AND il='SARAYBOSNA - BOSNA - HERSEK'; -- 26 UPDATE programs SET il='SARAYBOSNA-BOSNA HERSEK' WHERE il='SARAYBOSNA - BOSNA - HERSEK'; -- 15 -- ── 4) il: boş 110 satırın 95'i (kalan 15 bilerek NULL) ───────────────── UPDATE programs SET il = CASE WHEN universite LIKE '%(TİRAN-ARNAVUTLUK)%' THEN 'TİRAN-ARNAVUTLUK' WHEN universite LIKE '%(ÜSKÜP-MAKEDONYA)%' THEN 'ÜSKÜP-KUZEY MAKEDONYA' WHEN universite LIKE '%(SARAYBOSNA-BOSNA-HERSEK)%' THEN 'SARAYBOSNA-BOSNA HERSEK' WHEN universite LIKE '%(BİŞKEK-KIRGIZİSTAN)%' THEN 'BİŞKEK-KIRGIZİSTAN' WHEN universite LIKE '%(TÜRKİSTAN-KAZAKİSTAN)%' THEN 'TÜRKİSTAN-KAZAKİSTAN' WHEN universite='ULUSLARARASI BALKAN ÜNİVERSİTESİ' THEN 'ÜSKÜP-KUZEY MAKEDONYA' WHEN universite='ULUSLARARASI SARAYBOSNA ÜNİVERSİTESİ' THEN 'SARAYBOSNA-BOSNA HERSEK' WHEN universite='KIBRIS AMERİKAN ÜNİVERSİTESİ' THEN 'LEFKOŞA' END WHERE il IS NULL AND universite <> 'İÇİŞLERİ BAKANLIĞI VE MİLLİ SAVUNMA BAKANLIĞI ADINA SAĞLIK BİLİMLERİ ÜNİVERSİTESİNDE EĞİTİM ALACAKLAR'; -- beklenen: 95; kalan 15 MSB/İçişleri satırı NULL kalır (kaynak da boş veriyor) COMMIT; ``` **Doğrulama sorguları (düzeltmeden sonra koşulur):** ```sql SELECT COUNT(DISTINCT il) FROM programs; -- beklenen 88 (81 TR + 5 KKTC şehri + ... ) SELECT COUNT(*) FROM programs WHERE il IS NULL; -- beklenen 15 SELECT COUNT(*) FROM programs WHERE il='KIBRIS'; -- beklenen 0 SELECT COUNT(*) FROM programs WHERE unitur='DEVLET' AND (universite LIKE 'KIBRIS AMERİKAN%' OR universite LIKE '%BALKAN%' OR universite LIKE '%SARAYBOSNA%'); -- beklenen 0 SELECT COUNT(DISTINCT il) FROM programs WHERE unitur IN ('DEVLET','VAKIF','VAKIF MYO'); -- beklenen 81 ← T7'nin kabul kriteri SELECT SUM(kontenjan2026), SUM(yerlesen2026) FROM programs; -- düzeltmeden ÖNCEKİYLE AYNI olmalı ``` **Geri alma:** `data/yokatlas.db.yedek-2026-09-22` dosyasını geri kopyala. Betik yalnız 4 kolonsuz alana dokunuyor, sıralama/kontenjan verisine hiç dokunmuyor — son doğrulama sorgusu bunu kanıtlar. **Birlikte gitmesi gereken kod değişikliği (tek satır):** `/il/[slug]` ve il faceti `AND unitur IN ('DEVLET','VAKIF','VAKIF MYO')` ile süzülmeli; yoksa `/il/uskup-kuzey-makedonya` üretilir. Sihirbazın "Vakıf" kovası da `unitur IN ('VAKIF','VAKIF MYO')` olmalı (`src/lib/db.ts:79`) — bu **ayrı bir ürün kararı**: KKTC programları havuzdan çıkarsa vakıf havuzu 9.351 → 8.095 satıra iner. **[BİLAL]** değil, `urun-yoneticisi`/`ceo` kararı. **İdempotens uyarısı:** betik koşulduktan sonra `unitur-onar.ts`'in çelişki koruması (`aynı üniversitede birden fazla unitur`) hâlâ geçer — Balkan/Saraybosna'nın iki ad hattı farklı `universite` dizgileri olduğu için ayrı gruplar sayılıyor [K]. --- ## 4. Görev 2 — vakıf burs etiketi (A5) ### Tek cümlelik cevap **Etiket YÖK Atlas API'sinde ayrı bir alan değil, program adının (`birimAdi`) içinde geliyor; veri elimizde ve kırılım yapılabilir — bugün eksik olmasının sebebi `refresh.ts`'in `--isim-esitle` bayrağının varsayılan kapalı olması, kaynağın etiketi vermemesi değil.** ### Kanıt zinciri **(1) Kodun kendi belgesi** — `scripts/refresh.ts:37-39`: > `--isim-esitle`: kimliği AYNI olup yalnız adı farklı satırlarda (API burs/dil eki veriyor: "Tıp (İngilizce) (Burslu)") isim API'den yazılır. **Varsayılan KAPALI**: isim, sihirbaz kategorilerini ve bölüm sayfası başlığını besler. Yani etiketin kaynağı biliniyor ve yazılmaması **bilinçli bir seçim**. Gerekçe de meşru: `isim` sihirbaz kategorilerini besliyor, "Tıp (Burslu)" ile "Tıp" farklı kategoriye düşebilir. **(2) API isteği burs boyutunu tanıyor** — `refresh.ts:181` filtre nesnesinde `bursOraniId: null` var; yani uç nokta burs oranına göre süzmeyi destekliyor. Ama yanıt kaydından tutulan alanlar listesinde (`TUTULAN_ALANLAR`, satır 124-148) burs alanı **yok** — ham `.jsonl.gz` dosyası kaydın tamamını yazıyor (`refresh.ts:378`), ince kayıt değil, ama `data/ham/` bu makinede **mevcut değil** (`.gitignore:48`), bu yüzden API yanıtında ayrı bir `bursOrani` alanı olup olmadığı **yerel olarak doğrulanamadı** [doğrulanmalı — tek `--ham` koşusuyla ya da 1 isteklik `size:1` sorgusuyla 2 dakikada kapanır]. **(3) Asıl kanıt — satır yaşına göre etiketlilik.** Yeni satırlar `INSERT`'te `r.birimAdi`'yi doğrudan yazıyor (`refresh.ts:677`), eski satırların adı hiç güncellenmiyor: ```sql SELECT CASE WHEN sira2025 IS NOT NULL OR kontenjan2025 IS NOT NULL THEN '2025te de vardi' ELSE '2026da yeni' END durum, COUNT(*) n, SUM(isim LIKE '%(Burslu)%' OR isim LIKE '%İndirimli)%' OR isim LIKE '%(Ücretli)%') etiketli FROM programs WHERE kontenjan2026 IS NOT NULL AND onlisans=0 AND unitur LIKE 'VAKIF%' GROUP BY 1; ``` | Satır | n | Etiketli | Oran | |---|---|---|---| | 2026'da **yeni açılan** (isim API'den yazıldı) | 348 | **326** | **%93,7** | | 2025'te de **var olan** (isim 2021 CSV'sinden) | 4.427 | 628 | %14,2 | Aynı veri, aynı kurumlar, aynı yıl — tek fark satırın INSERT mi UPDATE mi yolundan geçtiği. **API etiketi veriyor; biz yazmıyoruz.** **(4) `veri_meta` kaydı da aynı şeyi gösteriyor.** `kimlik_onar:*` kayıtlarının `yeni.isim` alanları API'den geldiği gibi duruyor: `"Ebelik (Burslu)"`, `"Ebelik (%50 İndirimli)"`, `"Siber Güvenlik (Burslu) (İngilizce)"`, `"Endüstri Mühendisliği (Ücretli)"`, `"Anestezi (%25 İndirimli)"` [D]. Etiket sözlüğü tam: **Burslu · %75/%50/%25 İndirimli · Ücretli**. ### Bugünkü kırılım (bildirilen rakamlar doğrulandı) ```sql SELECT CASE WHEN unitur LIKE 'VAKIF%' THEN 'Vakıf' WHEN unitur='KKTC' THEN 'KKTC' WHEN unitur='DEVLET' THEN 'Devlet' ELSE unitur END grup, SUM(onlisans=0) lisans, SUM(onlisans=0 AND isim LIKE '%(Burslu)%') burslu, SUM(onlisans=0 AND isim LIKE '%İndirimli)%') indirimli, SUM(onlisans=0 AND isim LIKE '%(Ücretli)%') ucretli, SUM(onlisans=0 AND isim NOT LIKE '%(Burslu)%' AND isim NOT LIKE '%İndirimli)%' AND isim NOT LIKE '%(Ücretli)%') etiketsiz FROM programs WHERE kontenjan2026 IS NOT NULL GROUP BY 1; ``` | Grup | 2026 lisans | Burslu | İndirimli | Ücretli | **Etiketsiz** | |---|---|---|---|---|---| | Devlet | 6.505 | 9 | 39 | 13 | 6.444 (beklenen — devlette burs etiketi olmaz) | | **Vakıf** | 4.775 | 339 | 416 | 199 | **3.821 (%80,0)** | | **KKTC** | 849 | 39 | 51 | 12 | **747 (%88,0)** | | Yurt dışı kamu | 98 | 14 | 31 | 19 | 34 | Yazarların bildirdiği 3.821/4.775 ve (873 payda ile) 768/873 rakamları **doğrulandı**; payda farkı (849 vs 873) yalnız KKTC evreninin `unitur='KKTC'` mi yoksa `+ Kıbrıs Amerikan` mı tanımlandığına bağlı — A4 düzeltmesinden sonra iki tanım birleşiyor. ### `data/kaynak/` altındaki dosyalarda burs bilgisi var mı **Yok.** Oradaki iki dosya kurum düzeyinde YÖK istatistiği: - `yok-istatistik-ogrenci-sayilari-2025-2026.xls` — sütunları: `Okuyan Önlisans · Okuyan Lisans · Okuyan Yükseklisans · Okuyan Doktora · Genel Toplam`, satırları üniversite × il × (DEVLET/VAKIF/KKTC). - `yok-istatistik-ogretim-elemani-sayilari-2025-2026.xls` — öğretim elemanı sayıları. İkisi de **program düzeyinde değil**, burs/ücret alanı içermiyor (JasperReports dökümü; `file` + `strings` ile başlık satırları okundu, dosyalar değiştirilmedi). Bu dosyalar A5 için kullanılamaz; başka bir işe (kurum büyüklüğü/öğrenci-öğretim üyesi oranı içerikleri) yarayabilir. ### Yazarlara verilecek net cevap > **Kırılım bugün yapılamaz, ama kalıcı olarak imkânsız değil.** Veri kaynakta var; DB'ye tek bir refresh koşusuyla girer. **Bu koşu yapılana kadar hiçbir yazıda burslu/%50/ücretli dağılımı verilmesin** — bugünkü sayımlar (%80'i etiketsiz) gerçek dağılım değil, tazelenmemiş adların sayımıdır ve yayımlanırsa **yanlış** olur. ### Bu koşunun bedeli ve riski (Bilal/CTO için) ```sh # ÖNCE kopyada: ham dosya yoksa API'den tek geçiş (~22 sn/21.493 kayıt, 0,5 sn/sayfa) cp data/yokatlas.db /tmp/deneme.db pnpm refresh --yil 2026 --db /tmp/deneme.db --isim-esitle pnpm veri-kalite --yil 2026 --db /tmp/deneme.db --onceki data/yokatlas.db ``` - **Etkilenecek satır:** kabaca 10.164 (K10'daki ayırt edilemeyen ad grupları); kesin sayıyı koşunun `"yalnız adı (burs/dil eki) farklı N satır"` satırı verir (`refresh.ts:609`). - **Risk — küçümsenmemeli:** `isim` sihirbaz kategorilerini (`src/lib/kategoriler.ts`) ve `/bolum/` başlıklarını besliyor. "Bilgisayar Mühendisliği" → "Bilgisayar Mühendisliği (Burslu)" olursa kategori eşlemesi ve bölüm slug'ı kayabilir; **kaydedilmiş raporlardaki program adları da tutmayabilir.** Bu yüzden doğru sıra: (1) kopyada koş, (2) kategori eşleme farkını say, (3) gerekirse okuyan tarafa "etiketi kırp" normalizasyonu ekle, (4) ancak sonra gerçek DB. **Bu gece yapılmaz.** - **Daha ucuz alternatif:** `isim`'e hiç dokunmadan ayrı bir `burs` kolonu türetmek (`Burslu/%75/%50/%25/Ücretli/—`). Ama bu §3.5'te reddettiğim "yeni kolon" yoluna girer ve kategori riskini de çözmez, sadece erteler. **Önerim: `--isim-esitle` + okuyan tarafta etiket kırpma.** --- ## 5. Kapsam dışı ama yayını ilgilendiren bulgu: doluluk oranları yukarı sapmalı Görevde yok, ama bu gece yayımlanan rakamları etkilediği için rapora giriyor. ```sql SELECT COUNT(*) FROM programs WHERE kontenjan2025 IS NOT NULL AND gk2025 IS NOT NULL AND kontenjan2025 <> gk2025; -- 0 → kontenjan = YALNIZ genel kontenjan SELECT COUNT(*) FROM programs WHERE yerlesen2025 IS NOT NULL AND gkY2025 IS NOT NULL AND yerlesen2025 <> gkY2025; -- 7.989 → yerlesen = gkY+sgyY+dprmY+obkY ``` `scripts/refresh.ts:212-217` `yerlesen()` dört kota türünü toplarken, `kontenjan` kolonuna yalnız `r.kontenjan` (genel kontenjan) yazılıyor. Pay ile payda aynı evreni saymıyor: | Grup (2026 lisans) | Kontenjan | Yerleşen | Doluluk | `yerlesen>kontenjan` satır | |---|---|---|---|---| | Devlet | 280.131 | 286.150 | **%102,1** | **5.618** | | Vakıf | 99.638 | 78.475 | %78,8 | — | | KKTC | 13.162 | 7.923 | %60,2 | **0** | - **KKTC ve vakıf rakamları güvende** — KKTC'de okul birincisi/şehit-gazi/depremzade kontenjanı olmadığı için sapma sıfır; `kktc-universiteleri-okunur-mu` yazısının ÖSYM ile birebir uyuşması bunu bağımsız olarak doğruluyor (13.162/7.923 ÖSYM Tablo 1 ile aynı). - **Devlet doluluk rakamları sapmalı.** `bilgisayar-mi-yazilim-muhendisligi-mi` yazısındaki "devlet YM **%102,1**" tam olarak bu sapmadır; %100'ü aşan her doluluk rakamı bu hatanın imzasıdır. - **En küçük düzeltme yazı tarafında:** doluluk hesabında paydayı da tüm kota türlerini kapsayacak şekilde kurmak mümkün değil (toplam kontenjan kolonu DB'de yok) → bu sezon **devlet için doluluk rakamı yayımlanmasın**, ya da "genel kontenjana göre" kaydıyla ve %100 üstü değerler açıklanarak verilsin. Kalıcı düzeltme `kontenjanToplam` kolonu gerektirir → yeni yılın refresh'ine bağlanmalı, bu gece değil. - **Backlog önerisi:** `veri-kalite.ts`'e **ZORUNLU** kontrol olarak `yerlesen>kontenjan` oranı eklensin; bugün böyle bir kontrol yok (`scripts/veri-kalite.ts:145-150` yalnız `unitur boş` / `il boş` **bilgi** satırı basıyor, çıkış kodunu etkilemiyor). --- ## 6. Backlog önerileri (`urun-yoneticisi`ne) | Öneri | Gerekçe | Boy | |---|---|---| | **BACKLOG #9'u genişlet:** "3 yurt dışı/KKTC üniversitesi DEVLET etiketli" → gerçek kapsam **100 satır + 179 `il='KIBRIS'` + 110 boş `il` + 26 yanlış şehir**; çözüm §3.6 betiği | bugünkü madde kapsamı gerçeğin üçte biri | S | | **T34 kabul kriteri:** `SELECT COUNT(DISTINCT il) FROM programs WHERE unitur IN ('DEVLET','VAKIF','VAKIF MYO')` = **81** ve `COUNT(*) WHERE il IS NULL` = **15** | T7/#18'in önkoşulu, komutla doğrulanabilir | — | | **Yeni:** `refresh.ts --yer-esitle` (il/fakulte senkronu, önce listeler sonra yazar) | yoksa aynı kir 2027'de yeniden birikir | M | | **Yeni:** `veri-kalite.ts`'e 3 ZORUNLU kontrol — (a) aynı baz kurum adında iki `unitur`, (b) TR dışı `il` × TR `unitur` çelişkisi, (c) `yerlesen>kontenjan` | üçü de bu gece elle bulundu; otomatik olmalı | S | | **A5 sonrası:** `--isim-esitle` koşusu + kategori eşleme farkının sayımı | T11'in önkoşulu | M | --- ## 7. Engeller 1. **`data/ham/2026-2026-09-21.jsonl.gz` bu makinede yok** (`.gitignore:48`, dizin hiç yok). API yanıtında ayrı bir `bursOrani`/`ogrenimTuru` alanı olup olmadığını yerel olarak doğrulayamadım — A5'in cevabı bundan bağımsız (etiket `birimAdi` içinde geliyor, §4/3 kanıtı), ama "ayrıca ayrı kolon da var mı" sorusu açık kaldı. 2. **Disk.** Kural gereği DB kopyası alınmadı, betik denemesi yapılmadı; §3.6 ve §4'teki komutlar **hiç koşulmadı**, yalnız yazıldı. 3. **ÖSYM 2026 kılavuzu açılamadı** (yazar 14 de aynı engele takılmış: PDF metin katmanı çıkarılamıyor, makinede `poppler` yok). "ODTÜ Yazılım Mühendisliği Ankara'da değil, Kuzey Kıbrıs'ta" iddiası bu yüzden kılavuz maddesine değil, (a) ODTÜ KKK'nin kendi sayfasına ve (b) 45/5 Burslu-%50 kontenjan desenine dayanıyor → **[doğrulanmalı]** işaretli kaldı. --- ## Bilal'den istenen (3 madde) 1. **§3.6 düzeltme betiğine onay** — 100 `unitur` + 320 `il` satırını düzeltir, sıralama/kontenjan verisine dokunmaz, geri alma tek `cp`. Onaylarsan betiği `veri/il-temizligi` dalında yazıp kopyada doğrularım; T7 (`/il/[slug]`) ve #18 yazısı bu onaya bağlı. *(karar: 3 dk)* 2. **Ürün kararı: sihirbazın "Vakıf (burslu olursa)" adımı KKTC ve yurt dışı programlarını kapsasın mı?** Bugün kapsıyor (1.256 satır, havuzun %13,4'ü) ve öğrenciye söylenmiyor. Kapsamasın dersen vakıf havuzu 9.351 → 8.095'e iner. *(karar: 2 dk)* 3. **Devlet doluluk rakamı bu sezon yayımlanmasın** onayı — `bilgisayar-mi-yazilim-muhendisligi-mi` yazısındaki "%102,1" ve "%99,8" kota asimetrisinden sapmalı (§5); yazı yayına girmeden `rehber-editoru` o iki rakamı çıkarsın ya da "genel kontenjana göre" kaydıyla versin. *(karar: 2 dk, uygulama: 10 dk)*