merge: veri/2026-entegrasyon — sitedeki taban sıraları 2026 yerleştirme verisine geçti
Some checks failed
Deploy / deploy (push) Has been cancelled

Yıl sözleşmesi (veri-yillari.ts, EFEKTIF_SIRA_SQL), P-A veri betikleri ve
yıllık güncelleme runbook'u, yokatlas.db 2026 verisi (arşivli), okuyan taraf
yılsız Program tipi, metinler + meraklısına şeması, yazdırma dipnotu.

Denetim: kod-denetcisi 2026-09-21 — koşullu merge; yayın sonrası prod'da
pnpm tadimlik --force, kayıtlı liste tabanı yenileme takip işi.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-21 18:51:57 +03:00
50 changed files with 2458 additions and 518 deletions

3
.gitignore vendored
View File

@@ -43,3 +43,6 @@ next-env.d.ts
# Python
__pycache__/
*.pyc
# YÖK Atlas ham API çekimleri (scripts/refresh.ts; ~yıllık, büyük, yeniden üretilebilir)
data/ham/

Binary file not shown.

View File

@@ -0,0 +1,230 @@
# Yıllık veri güncellemesi (YÖK Atlas → `data/yokatlas.db`)
Her yıl yerleştirme sonuçları açıklandıktan sonra **bir kez** yapılır. Bu sayfayı
yukarıdan aşağıya izleyen biri, betiklerin içini bilmeden güncellemeyi
tamamlayabilir. Toplam süre: yaklaşık **30 dakika** (çoğu bekleme ve gözle kontrol).
> Ürünün bütün değeri bu verinin doğruluğuna dayanır. Yanlış bir taban sıralaması,
> bir öğrencinin yanlış tercih yapması demektir. Bu yüzden betikler şüphede
> **durur**; "DURDU:" ile başlayan bir mesaj hata değil, emniyet kilididir.
> Kilidi aşmaya çalışma — mesajı oku, aşağıdaki "Bir şey durursa" tablosuna bak.
Örneklerde yeni yıl `2027` diye yazıldı; o yıl hangisiyse onu yaz.
## 0. Ne zaman?
- ÖSYM yerleştirme sonuçlarını açıklar (genelde **Ağustos sonu**), YÖK Atlas verisini
birkaç gün–birkaç hafta sonra günceller. Tarih her yıl değişir → **doğrulanmalı**.
- Erken koşmanın zararı yok: YÖK Atlas henüz güncellenmediyse 3. adım
"`… hâlâ 2026 verisi`" diyerek **hiçbir şey yazmadan** durur. Bir hafta sonra tekrar dene.
- Dosya indirmen gerekmez. Veri, YÖK Atlas'ın herkese açık tercih kılavuzu
servisinden (`https://yokatlas.yok.gov.tr/api/tercih-kilavuz/search`) betikle çekilir:
43 istek, aralarında yarım saniye — kaynağı yormaz. Betiği art arda defalarca koşma;
ilk çekimden sonra hep kaydedilmiş ham dosyayı (`--ham`) kullan.
## 1. Hazırlık (5 dk)
```bash
git switch -c veri/2027 # main'de çalışma
```
`src/lib/veri-yillari.ts` dosyasında **tek satırı** değiştir:
```ts
export const SON_YIL = 2027;
```
Betikler ve site yılı yalnız buradan okur. Betiğe verdiğin `--yil` bununla aynı
değilse betik durur (yanlış kolona yazmayı önler).
## 2. Kopya al — asıl dosyaya dokunma (1 dk)
Dev sunucusu açıksa kapat. Sonra:
```bash
mkdir -p /tmp/veri-2027
sqlite3 data/yokatlas.db ".backup '/tmp/veri-2027/once.db'" # güncelleme ÖNCESİ hâl (geri dönüş + karşılaştırma)
cp /tmp/veri-2027/once.db /tmp/veri-2027/yeni.db # üzerinde çalışılacak kopya
```
Bundan sonraki her adım **`/tmp/veri-2027/yeni.db`** üzerinde koşar. `data/yokatlas.db`
ancak 8. adımda, her şey yeşilken değişir.
## 3. Yeni yılı çek (3 dk)
```bash
pnpm refresh --yil 2027 --db /tmp/veri-2027/yeni.db
```
Ne yapar: şemaya `sira2027/puan2027/kontenjan2027/yerlesen2027` kolonlarını ekler,
**yıl kilidini** sınar, tüm kılavuzu çeker, ham hâlini `data/ham/2027-<tarih>.jsonl.gz`
olarak saklar (git'e girmez), sonra tek seferde DB'ye yazar.
Görmen gerekenler:
```
Yıl kilidi: basariSirasi1 = sira2026 → 480/480 (%100.0); eksiz basariSirasi = sira2026 → 0/480 (%0.0)
Çekilen: 21… kayıt, benzersiz kod: 21…, kodsuz: 0, API toplamı: 21… ← üç sayı aynı
Güncellenen: 19…, eklenen: 1…, atlanan: 0
```
- **Yıl kilidi** şunu kanıtlar: servisin "bir önceki yıl" diye verdiği sıralar bizim
2026 kolonumuzla birebir aynı **ve** "bu yıl" diye verdikleri ondan farklı. Yani
yazacağımız veri gerçekten 2027'dir. (Servisteki `yil` alanına güvenilmez; 2026'da
bu alan yüzünden 2025 verisi bozulmak üzereydi.)
- "atlanan" 0 değilse satırlar altında listelenir: kaynağın puan türü vermediği yeni
programlardır; uydurulmaz. Sayıyı not et.
- Bu betik mevcut programların **eski yıl kolonlarına dokunmaz**; dokunursa kendi
sağlaması bunu yakalar ve her şeyi geri alır.
Aynı işlemi servise gitmeden tekrarlamak için (ör. 8. adımda):
`pnpm refresh --yil 2027 --db <db> --ham data/ham/2027-<tarih>.jsonl.gz`
## 4. Geçmiş boşluklarını doldur (1 dk)
```bash
pnpm gecmis-doldur --yil 2027 --db /tmp/veri-2027/yeni.db
```
Kılavuz kodu değişen ya da yeni koda taşınan programların eski yıllarını tamamlar.
Yalnız **boş** alanı doldurur; dolu değeri asla ezmez, tahmin yapmaz. Kaynak sırası:
servisin kendi geçmiş alanları → servisin bildirdiği eski kod → aynı üniversitede
bire bir aynı adlı ve en az bir yılı kanıtla örtüşen tek satır.
Çıktının sonundaki "**Dolu olup kaynağın FARKLI söylediği alanlar**" listesine bak.
Birkaç onluk fark olağandır (kaynak eski sıraları ufak düzeltmelerle yeniden yayımlıyor;
2026'da `sira2024` farklarının %95'i on binde birin altındaydı). **Binlerce satırda büyük
fark** görürsen dur ve veri mühendisine sor.
## 5. Kılavuzdan çıkan programlar: önce sadece raporla (1 dk)
```bash
pnpm eski-arsivle --yil 2027 --db /tmp/veri-2027/yeni.db --kuru
```
Hiçbir şey yazmaz. Şunu söyler: bu yılın kılavuzunda **olmayan** kaç satır var ve
bunlar siteden kalkarsa hangi `/bolum/...` ve `/universite/...` sayfaları tamamen
kapanır (adlarıyla). Bu liste **[BİLAL] kararıdır** — sayfa kapanışı SEO'yu etkiler,
gerekirse yönlendirme (redirect) eklenir.
Onaydan sonra gerçek taşıma (silmez; satırlar `programs_arsiv` tablosuna geçer):
```bash
pnpm eski-arsivle --yil 2027 --db /tmp/veri-2027/yeni.db
```
Neden gerekli: arşivlenmezse eski kodlu satırlar listelerde bir yıl önceki sırayla
görünmeye devam eder; öğrenci o kodu tercih ekranında bulamaz, kodu değişen program
iki kez listelenir.
## 6. İndeks + dosyayı toparla (1 dk)
```bash
pnpm db:index --db /tmp/veri-2027/yeni.db --vacuum
```
Son satırda `USING INDEX idx_programs_tur_onlisans_efektif` yazmalı. Yazmıyorsa betik
zaten hata verir: sitedeki sıralama aralığı sorguları yavaşlar.
## 7. Kalite kapısı (1 dk)
```bash
pnpm veri-kalite --yil 2027 --db /tmp/veri-2027/yeni.db --onceki /tmp/veri-2027/once.db --arsivli
echo "çıkış kodu: $?" # 0 olmalı
```
(`--arsivli`'yi yalnız 5. adımda gerçek arşivlemeyi yaptıysan ekle.)
- Her satır `GEÇTİ`, `KALDI`, `UYARI` ya da `bilgi` ile başlar. **Tek bir `KALDI` varsa
yayına çıkılmaz**; çıkış kodu 1 olur.
- En önemli iki satır:
- `önceki DB: geçmiş yıllar korunmuş — … değişen/silinen satır: 0`
- `sira2027/sira2026 ∈ [0,5; 2] — … (%9x)` — programların büyük çoğunluğunun sırası
bir yılda yarıya inmez, iki katına çıkmaz. Oran düşükse yıllar kaymıştır.
- `UYARI`'lar bilgilendirir (ör. ili boş 110 program, yetim net satırları); yeni bir
uyarı türü ya da sayıda sıçrama varsa veri mühendisine göster.
Elle 3 program seç (biri çok bilinen: ör. Boğaziçi Bilgisayar Mühendisliği), YÖK Atlas
sitesindeki 2027 taban sırasıyla karşılaştır:
```bash
sqlite3 /tmp/veri-2027/yeni.db "SELECT id, universite, isim, sira2026, sira2027, puan2027 FROM programs WHERE isim LIKE 'Bilgisayar Mühendisliği%' AND universite LIKE 'BOĞAZİÇİ%';"
```
## 8. Yerine koy ve siteyi kontrol et (5 dk)
```bash
cp /tmp/veri-2027/yeni.db data/yokatlas.db
rm -f data/yokatlas.db-wal data/yokatlas.db-shm # eski dosyanın artıkları; yenisi temiz (checkpoint yapılmış)
pnpm build # katalog sayfaları yeni veriden üretilir; hata vermemeli
```
Netler (yerleşen son kişinin netleri) ayrı bir servisten gelir ve genelde daha geç
güncellenir; hazır olduğunda:
```bash
pnpm detay --yil 2027 --db data/yokatlas.db # ~127 istek, ~2 dk; yıl sağlaması tutmazsa durur
```
`--kirilim` bayrağını **kullanma**: kontenjan kırılımını (okul birincisi, şehit-gazi…)
sitede okuyan kod yok ve servis bu alanları Eylül 2026'dan beri yayımlamıyor. Bayrak
verilse bile alanlar yoksa adım kendini durdurur, mevcut değerlerin üstüne boş yazmaz.
Yıl değişince koddaki metinler (`"son 6 yıl"`, başlıklardaki yıl vb.) `veri-yillari.ts`
sabitlerinden türediği için kendiliğinden güncellenir. Elle yıl yazılmış yer kaldıysa bul:
```bash
grep -rnE "sira20[0-9]{2}|20(2[1-9])[–-]20" src scripts --include='*.ts' --include='*.tsx' | grep -v veri-yillari
```
Tadımlık havuzu ve kayıtlı raporlar eski sıralarla üretilmiştir → yayından sonra prod'da
`pnpm tadimlik --force` (Bilal koşar; ayrıntı `scripts/tadimlik-uret.ts` başlığında).
## 9. Commit ve yayın
```bash
git add src/lib/veri-yillari.ts data/yokatlas.db
git commit -m "veri: 2027 yerleştirme verisi"
```
`data/ham/` git'e girmez (yerelde sakla; bir sonraki yıl karşılaştırma için işe yarar).
Push öncesi `AGENTS.md`'deki "Push öncesi build zorunlu" kuralı geçerli. Yayın kararı Bilal'in.
## Geri alma
| Ne zaman | Nasıl | Süre |
|---|---|---|
| 8. adımdan önce | Hiçbir şey yapma; `data/yokatlas.db` hiç değişmedi. `/tmp/veri-2027/` silinebilir. | 0 |
| 8. adımdan sonra, commit'ten önce | `git checkout -- data/yokatlas.db data/yokatlas.db-wal data/yokatlas.db-shm` | 1 dk |
| Commit/yayından sonra | `git revert <commit>` + yeniden yayın. `SON_YIL` da aynı commit'te geri döner; site bir önceki yılın verisiyle tutarlı çalışır. | 10 dk |
| Yalnız arşivlemeyi geri almak | `INSERT INTO programs SELECT * FROM programs_arsiv; DELETE FROM programs_arsiv;` (kopyada dene, sonra `pnpm db:index`). Netler yeni kodda kalır; zararsız. | 5 dk |
`/tmp/veri-2027/once.db` dosyasını yayından bir hafta sonrasına kadar silme.
## Bir şey durursa
| Mesaj | Anlamı | Ne yap |
|---|---|---|
| `--yil … ≠ SON_YIL` | 1. adım atlanmış | `veri-yillari.ts`'i güncelle |
| `eksiz alanları hâlâ <geçen yıl> verisi` | YÖK Atlas henüz yeni yılı yayımlamamış | Bekle, bir hafta sonra tekrar dene |
| `"…1" alanları DB'deki … verisiyle eşleşmiyor` | Servis bir yıl daha ilerlemiş (bir yılı atladık) **ya da** servis biçim değiştirmiş **ya da** yanlış DB | Yazma. Veri mühendisine göster |
| `Benzersiz kod sayısı … ≠ API toplamı` | Çekim sırasında servis sayfaları kaydırdı | Birkaç dakika sonra 3. adımı tekrarla (DB'ye bir şey yazılmadı) |
| `Aynı üniversitede birden fazla unitur` | Bir kurumun türü iki farklı yazılmış | Listelenen kurumları veri mühendisine göster; `scripts/unitur-onar.ts` |
| `Güncel satır sayısı … refresh kaydıyla uyuşmuyor` (arşivle) | refresh yarım kalmış | 3. adımı tekrarla; arşivleme bilerek çalışmıyor |
| `veri-kalite` içinde `KALDI` | O kontrol tutmadı | Yayına çıkma; satırdaki sayıyı ve örnek kodları veri mühendisine ilet |
## Betikler ve dokundukları
| Betik | Yazar mı? | Ne yazar |
|---|---|---|
| `scripts/yokatlas-goc.ts` (`pnpm yokatlas:goc`) | evet | Yalnız şema: yıl kolonları, `son_kilavuz_yili`, `eski_kod`, `programs_arsiv`, `veri_meta`. refresh bunu kendisi çağırır. |
| `scripts/refresh.ts` | evet | `sira/puan/kontenjan/yerlesen<yıl>`, `son_kilavuz_yili`, `eski_kod`; yeni programlar; ham dosya |
| `scripts/gecmis-doldur.ts` | evet | Yalnız **NULL** geçmiş yıl alanları + `eski_kod` |
| `scripts/eski-arsivle.ts` | `--kuru` ile hayır | Satır taşıma `programs` → `programs_arsiv`; `netler.program_id` yeni koda |
| `scripts/db-index.ts` | evet | İndeks, `--vacuum`, WAL checkpoint |
| `scripts/veri-kalite.ts` | **hayır** | Salt okunur; çıkış kodu 0/1 |
| `scripts/detay.ts` | evet | `netler`; `--kirilim` ile (alanlar varsa) bir önceki yılın kırılım kolonları |
Kolon sözlüğü: `son_kilavuz_yili` = programın görüldüğü son tercih kılavuzu (NULL = 2024
ve öncesi, bilinmiyor). `eski_kod` = kılavuz kodu değiştiyse bir önceki kod — kayıtlı
listelerdeki eski kodları yeni koda çevirmek için `SELECT id FROM programs WHERE eski_kod = ?`.

View File

@@ -10,6 +10,11 @@
"ingest": "tsx scripts/ingest.ts",
"refresh": "tsx scripts/refresh.ts",
"detay": "tsx scripts/detay.ts",
"yokatlas:goc": "tsx scripts/yokatlas-goc.ts",
"gecmis-doldur": "tsx scripts/gecmis-doldur.ts",
"eski-arsivle": "tsx scripts/eski-arsivle.ts",
"veri-kalite": "tsx scripts/veri-kalite.ts",
"db:index": "tsx scripts/db-index.ts",
"logolar": "tsx scripts/logo-indir.ts",
"db:push": "drizzle-kit push",
"db:generate": "drizzle-kit generate",

View File

@@ -1,44 +1,83 @@
// Sorgu yolundaki ifade indexi + WAL bakımı. Bir kez çalıştırılır
// (pnpm tsx scripts/db-index.ts); ingest/refresh sonrası da güvenle
// tekrar çalıştırılabilir (IF NOT EXISTS).
// Sorgu yolundaki ifade indexi + WAL bakımı. refresh sonrası (ve istenince tek
// başına) çalıştırılır; tekrar çalıştırmak güvenlidir.
//
// src/lib/db.ts tüm aralık sorgularını COALESCE(sira2025, sira2024)
// üzerinden yapar; SQLite ifade indexleri METİNSEL eşleştiği için buradaki
// ifade oradaki EFEKTIF_SIRA sabitiyle birebir aynı olmalıdır. Bu index
// olmadan her sorgu tur-partisyonunu tarayıp temp B-tree ile sıralıyordu.
// src/lib/db.ts tüm aralık sorgularını EFEKTIF_SIRA_SQL üzerinden yapar;
// SQLite ifade indexleri METİNSEL eşleştiği için indeks ifadesi
// src/lib/veri-yillari.ts'teki sabitle birebir aynı olmalıdır — bu yüzden
// buraya elle yazılmaz, import edilir. Bu index olmadan her sorgu
// tur-partisyonunu tarayıp temp B-tree ile sıralıyordu.
//
// Kullanım: pnpm db:index --db data/yokatlas.db [--vacuum]
// --vacuum: arşivleme gibi büyük silmelerden sonra dosyayı küçültür (site kapalıyken).
import path from "node:path";
import Database from "better-sqlite3";
import { EFEKTIF_SIRA_SQL } from "../src/lib/veri-yillari";
import { bayrakVar, calistir, dbAc, dbArgumani, dur, type Db } from "./yokatlas-goc";
const db = new Database(path.join(process.cwd(), "data", "yokatlas.db"));
export const EFEKTIF_INDEKS = "idx_programs_tur_onlisans_efektif";
db.exec(`
CREATE INDEX IF NOT EXISTS idx_programs_tur_onlisans_efektif
ON programs (tur, onlisans, COALESCE(sira2025, sira2024));
`);
/**
* Efektif sıra indeksini güncel ifadeyle kurar. Eski yılın ifadesiyle kurulmuş
* aynı adlı indeks varsa DROP edip yeniden kurar (yıl değişince ifade değişir).
*/
export function efektifIndeksiKur(db: Db): "kuruldu" | "yenilendi" | "guncel" {
const mevcut = db
.prepare("SELECT sql FROM sqlite_master WHERE type = 'index' AND name = ?")
.get(EFEKTIF_INDEKS) as { sql: string } | undefined;
let sonuc: "kuruldu" | "yenilendi" | "guncel" = "kuruldu";
if (mevcut) {
if (mevcut.sql.includes(EFEKTIF_SIRA_SQL)) return "guncel";
db.exec(`DROP INDEX ${EFEKTIF_INDEKS}`);
sonuc = "yenilendi";
}
db.exec(
`CREATE INDEX ${EFEKTIF_INDEKS} ON programs (tur, onlisans, ${EFEKTIF_SIRA_SQL})`
);
return sonuc;
}
// Programatik SEO sayfaları (src/lib/katalog.ts) bölüm/üniversite detaylarını
// ham ada birebir IN (...) eşleşmesiyle çeker; bu iki index olmadan her
// sayfa tam tablo taraması yapar.
db.exec(`
CREATE INDEX IF NOT EXISTS idx_programs_isim ON programs (isim);
CREATE INDEX IF NOT EXISTS idx_programs_universite ON programs (universite);
`);
// Uygulama DB'yi readonly açtığı için checkpoint yapamaz; şişen WAL'i
// (okuma başına ekstra maliyet) burada dosyaya geri yaz ve sıfırla.
db.pragma("wal_checkpoint(TRUNCATE)");
const plan = db
/** Sihirbaz/liste aralık sorgusunun planı (src/lib/db.ts dilimAra ile aynı biçim). */
export function efektifSorguPlani(db: Db): string[] {
const plan = db
.prepare(
`EXPLAIN QUERY PLAN
SELECT id FROM programs
WHERE tur = ? AND onlisans = ?
AND COALESCE(sira2025, sira2024) IS NOT NULL
AND COALESCE(sira2025, sira2024) BETWEEN ? AND ?
ORDER BY COALESCE(sira2025, sira2024) ASC LIMIT 10`,
AND ${EFEKTIF_SIRA_SQL} IS NOT NULL
AND ${EFEKTIF_SIRA_SQL} BETWEEN ? AND ?
ORDER BY ${EFEKTIF_SIRA_SQL} ASC LIMIT 10`
)
.all("SAYISAL", 0, 1000, 50000);
.all("SAYISAL", 0, 1000, 50000) as { detail: string }[];
return plan.map((p) => p.detail);
}
console.log("Sorgu planı:", JSON.stringify(plan, null, 2));
db.close();
if (process.argv[1]?.endsWith("db-index.ts")) {
calistir(() => {
const db = dbAc(dbArgumani());
console.log(`Efektif indeks (${EFEKTIF_SIRA_SQL}): ${efektifIndeksiKur(db)}`);
// Programatik SEO sayfaları (src/lib/katalog.ts) bölüm/üniversite
// detaylarını ham ada birebir IN (...) eşleşmesiyle çeker; bu iki index
// olmadan her sayfa tam tablo taraması yapar.
db.exec(`
CREATE INDEX IF NOT EXISTS idx_programs_isim ON programs (isim);
CREATE INDEX IF NOT EXISTS idx_programs_universite ON programs (universite);
`);
if (bayrakVar("vacuum")) {
db.exec("VACUUM");
console.log("VACUUM tamam.");
}
// Uygulama DB'yi readonly açtığı için checkpoint yapamaz; şişen WAL'i
// (okuma başına ekstra maliyet) burada dosyaya geri yaz ve sıfırla.
db.pragma("wal_checkpoint(TRUNCATE)");
const plan = efektifSorguPlani(db);
console.log("Sorgu planı:");
for (const satir of plan) console.log(` ${satir}`);
db.close();
if (!plan.some((s) => s.includes(EFEKTIF_INDEKS))) {
dur(`Sorgu planı ${EFEKTIF_INDEKS} indeksini kullanmıyor.`);
}
});
}

View File

@@ -1,45 +1,71 @@
/**
* Canlı YÖK Atlas API'sinden program detay verilerini çekip DB'ye işler:
* Canlı YÖK Atlas API'sinden program detay verilerini çekip DB'ye işler.
* İki BAĞIMSIZ adım:
*
* 1. Kontenjan türü kırılımı (son yerleştirme yılı, ör. 2025):
* Genel / Okul Birincisi / Şehit-Gazi / Deprem / 34 Yaş Üstü Kadın
* için kontenjan + yerleşen. Kaynak: POST /api/tercih-kilavuz/search
* (gk1/gkY1, obk1/obkY1, sgy1/sgyY1, dprm1/dprmY1, y34_1/y34Y1 —
* "1" son yerleştirme yılı demek; kayıttaki `yil` kılavuz yılıdır,
* veri yılı = yil - 1).
* A) Yerleşen son kişinin netleri (varsayılan; `--netler-atla` ile kapatılır)
* Kaynak: POST /api/netler/search → `netler` tablosu. Bu API her satırda
* verinin ait olduğu yılı `yil` alanında verir; betik bunu körlemesine
* kabul etmez: ilk sayfada `tabanPuan`, DB'deki puan<yil> ile ≥%95
* eşleşmiyorsa DURUR (yıl etiketi kaymış demektir).
*
* 2. Yerleşen son kişinin netleri (yıl bazında, tüm puan türleri):
* Kaynak: POST /api/netler/search → `netler` tablosu.
* B) Kontenjan türü kırılımı — YALNIZ `--kirilim` bayrağıyla (varsayılan KAPALI)
* Genel / Okul Birincisi / Şehit-Gazi / Deprem / 34 Yaş Üstü Kadın için
* kontenjan + yerleşen; API'nin `…1` alanları → <yil-1> kolonları.
* `--yil`'in kendi kırılım kolonları BİLEREK yok (src/ içinde okuyan kod
* yok; KARARLAR 2026-09-21).
* Emniyet: (1) refresh ile aynı YIL KİLİDİ; (2) beklenen on alanın HER
* BİRİ ilk sayfada en az bir kayıtta bulunmalı — API null alanları hiç
* göndermediği için "ilk kayıt" tek başına yanıltır; bir alan ilk sayfanın
* tamamında `undefined` ise API o alanı artık yayımlamıyordur ve adım
* HİÇBİR ŞEY yazmadan DURUR; (3) yazım `COALESCE(yeni, mevcut)` —
* mevcut değerin üstüne asla NULL yazılmaz.
* (Eylül 2026: API `gkY1/obk1/…` alanlarını yayımlamıyor → bu adım durur;
* 2025 kırılımı DB'de Ağustos 2026 çekiminden duruyor, korunur.)
*
* Yıl HİÇBİR adımda kılavuz kaydındaki `yil` alanından türetilmez.
*
* Not: YÖK Atlas 2026'da SPA'ya geçti; eski content/lisans-dynamic/*.php
* panelleri (ve onlarla birlikte kadın-erkek dağılımı) yayından kalktı.
* Cinsiyet verisi yeni API'lerin hiçbirinde yok.
*
* Kullanım: npx tsx scripts/detay.ts
* Kullanım:
* pnpm detay --yil 2026 --db /tmp/deneme.db
* pnpm detay --yil 2026 --db /tmp/deneme.db --kirilim --netler-atla
*/
import path from "node:path";
import Database from "better-sqlite3";
import type Database from "better-sqlite3";
import { ILK_YIL } from "../src/lib/veri-yillari";
import { yilKilidi, type KilavuzKaydi } from "./refresh";
import {
argOku,
bayrakVar,
calistir,
dbAc,
dbArgumani,
dur,
kolonAdlari,
yilArgumani,
type Db,
} from "./yokatlas-goc";
const KILAVUZ_API = "https://yokatlas.yok.gov.tr/api/tercih-kilavuz/search";
const NETLER_API = "https://yokatlas.yok.gov.tr/api/netler/search";
const PAGE_SIZE = 500;
const DELAY_MS = 300;
const DELAY_MS = 500;
type KilavuzRecord = {
kilavuzKodu: number;
eskiKilavuzKodu?: number | null;
yil: number;
gk1?: number | null;
gkY1?: number | null;
obk1?: number | null;
obkY1?: number | null;
sgy1?: number | null;
sgyY1?: number | null;
dprm1?: number | null;
dprmY1?: number | null;
y34_1?: number | null;
y34Y1?: number | null;
};
/** API alanı → DB kolon öneki. `…1` = bir önceki yerleştirme yılı. */
const KIRILIM_ALANLARI = [
["gk1", "gk"],
["gkY1", "gkY"],
["obk1", "obk"],
["obkY1", "obkY"],
["sgy1", "sgy"],
["sgyY1", "sgyY"],
["dprm1", "dprm"],
["dprmY1", "dprmY"],
["y34_1", "y34k"],
["y34Y1", "y34kY"],
] as const;
type KirilimKaydi = KilavuzKaydi & Record<string, unknown>;
type NetlerRecord = {
kilavuzKodu: number;
@@ -113,96 +139,9 @@ const NET_COLS = [
"ydtYdilNet",
] as const;
async function main() {
const db = new Database(path.join(process.cwd(), "data", "yokatlas.db"));
db.pragma("journal_mode = WAL");
// ── A) Netler ────────────────────────────────────────────────────────────
// ── 1. Kontenjan türü kırılımı ─────────────────────────────────────────
const first = await fetchPage<KilavuzRecord>(KILAVUZ_API, 0);
// API kaydındaki yil kılavuz yılı; "1" ekli alanlar bir önceki
// yerleştirme dönemine ait (2026 kılavuzu → 2025 verisi)
const veriYili = first.content[0].yil - 1;
const kontCols = [
`gk${veriYili}`,
`gkY${veriYili}`,
`obk${veriYili}`,
`obkY${veriYili}`,
`sgy${veriYili}`,
`sgyY${veriYili}`,
`dprm${veriYili}`,
`dprmY${veriYili}`,
`y34k${veriYili}`,
`y34kY${veriYili}`,
];
const mevcut = new Set(
(db.prepare("PRAGMA table_info(programs)").all() as { name: string }[]).map(
(c) => c.name
)
);
for (const col of kontCols) {
if (!mevcut.has(col)) {
db.exec(`ALTER TABLE programs ADD COLUMN ${col} INTEGER`);
}
}
const updateKont = db.prepare(`
UPDATE programs SET ${kontCols.map((c) => `${c} = ?`).join(", ")}
WHERE id = ?
`);
const exists = db.prepare("SELECT 1 FROM programs WHERE id = ?");
// Kılavuz kodu yıllar arasında değişebiliyor (eskiKilavuzKodu); netler
// API'si yeni kodla döndüğü için eşleşen DB id'sini haritada tutuyoruz.
const dbId = new Map<number, string>();
let kontUpdated = 0;
let kontMissing = 0;
const processKilavuz = db.transaction((records: KilavuzRecord[]) => {
for (const r of records) {
if (!r.kilavuzKodu) continue;
let id = String(r.kilavuzKodu);
if (!exists.get(id)) {
const eski = r.eskiKilavuzKodu ? String(r.eskiKilavuzKodu) : null;
if (eski && exists.get(eski)) {
id = eski;
} else {
kontMissing++;
continue;
}
}
dbId.set(r.kilavuzKodu, id);
updateKont.run(
num(r.gk1),
num(r.gkY1),
num(r.obk1),
num(r.obkY1),
num(r.sgy1),
num(r.sgyY1),
num(r.dprm1),
num(r.dprmY1),
num(r.y34_1),
num(r.y34Y1),
id
);
kontUpdated++;
}
});
console.log(
`Kontenjan kırılımı (${veriYili}): ${first.totalElements} kayıt, ${first.totalPages} sayfa…`
);
processKilavuz(first.content);
for (let p = 1; p < first.totalPages; p++) {
await sleep(DELAY_MS);
processKilavuz((await fetchPage<KilavuzRecord>(KILAVUZ_API, p)).content);
process.stdout.write(`\rkontenjan sayfa ${p + 1}/${first.totalPages}`);
}
console.log(
`\nKontenjan kırılımı: ${kontUpdated} güncellendi, ${kontMissing} DB'de yok`
);
// ── 2. Yerleşen son kişinin netleri ────────────────────────────────────
async function netlerAdimi(db: Db, yil: number, azamiSayfa: number): Promise<void> {
db.exec(`
CREATE TABLE IF NOT EXISTS netler (
program_id TEXT NOT NULL,
@@ -214,54 +153,165 @@ async function main() {
);
`);
const insertNet = db.prepare(`
INSERT OR REPLACE INTO netler (program_id, yil, ${NET_COLS.join(", ")})
VALUES (${["?", "?", ...NET_COLS.map(() => "?")].join(", ")})
`);
const first = await fetchPage<NetlerRecord>(NETLER_API, 0);
let netRows = 0;
// Yıl etiketi sağlaması: netler.tabanPuan(yil=Y) ≈ programs.puanY
const programKolonlari = new Set(kolonAdlari(db, "programs"));
const puanGetir = new Map<number, Database.Statement<unknown[]>>();
let kiyas = 0;
let esit = 0;
for (const r of first.content) {
if (!r.kilavuzKodu || !r.yil || num(r.tabanPuan) == null) continue;
if (r.yil < ILK_YIL || r.yil > yil || !programKolonlari.has(`puan${r.yil}`)) continue;
let st = puanGetir.get(r.yil);
if (!st) {
st = db.prepare(`SELECT puan${r.yil} AS p FROM programs WHERE id = ?`);
puanGetir.set(r.yil, st);
}
const satir = st.get(String(r.kilavuzKodu)) as { p: number | null } | undefined;
if (!satir || satir.p == null) continue;
kiyas++;
if (Math.abs(satir.p - (r.tabanPuan as number)) <= 0.01) esit++;
}
const oran = kiyas ? esit / kiyas : 0;
console.log(`Netler yıl sağlaması: tabanPuan = puan<yil> → ${esit}/${kiyas} (%${(oran * 100).toFixed(1)})`);
if (kiyas < 50 || oran < 0.95) {
dur(
"Netler API'sindeki `yil` etiketi DB'deki yıl kolonlarıyla örtüşmüyor " +
"(ya da karşılaştıracak veri yok). Netler yazılmadı."
);
}
// Mevcut değerin üstüne NULL yazılmaz: çakışmada COALESCE(yeni, eski).
const upsert = db.prepare(`
INSERT INTO netler (program_id, yil, ${NET_COLS.join(", ")})
VALUES (${["?", "?", ...NET_COLS.map(() => "?")].join(", ")})
ON CONFLICT(program_id, yil) DO UPDATE SET
${NET_COLS.map((c) => `${c} = COALESCE(excluded.${c}, ${c})`).join(",\n ")}
`);
const programVar = db.prepare("SELECT 1 FROM programs WHERE id = ?");
let yazilan = 0;
let programYok = 0;
let yilDisi = 0;
const processNetler = db.transaction((records: NetlerRecord[]) => {
for (const r of records) {
if (!r.kilavuzKodu || !r.yil) continue;
const id = dbId.get(r.kilavuzKodu) ?? String(r.kilavuzKodu);
insertNet.run(
if (r.yil < ILK_YIL || r.yil > yil) {
yilDisi++;
continue;
}
const id = String(r.kilavuzKodu);
// Yetim satır üretme: program DB'de yoksa (refresh koşulmamış / atlanmış) sayıp geç.
if (!programVar.get(id)) {
programYok++;
continue;
}
upsert.run(
id,
r.yil,
r.puanTuru?.trim() ?? null,
...NET_COLS.slice(1).map((c) => num(r[c as keyof NetlerRecord]))
);
netRows++;
yazilan++;
}
});
const netFirst = await fetchPage<NetlerRecord>(NETLER_API, 0);
console.log(
`Netler: ${netFirst.totalElements} kayıt, ${netFirst.totalPages} sayfa…`
);
processNetler(netFirst.content);
for (let p = 1; p < netFirst.totalPages; p++) {
const sayfa = Math.min(first.totalPages, azamiSayfa);
console.log(`Netler: ${first.totalElements} kayıt, ${first.totalPages} sayfa${sayfa < first.totalPages ? ` (yalnız ilk ${sayfa})` : ""}…`);
processNetler(first.content);
for (let p = 1; p < sayfa; p++) {
await sleep(DELAY_MS);
processNetler((await fetchPage<NetlerRecord>(NETLER_API, p)).content);
process.stdout.write(`\rnetler sayfa ${p + 1}/${netFirst.totalPages}`);
process.stdout.write(`\rnetler sayfa ${p + 1}/${sayfa}`);
}
console.log(`\nNetler: ${netRows} satır yazıldı`);
const ozet = db
.prepare(
`SELECT
(SELECT COUNT(*) FROM programs WHERE gkY${veriYili} IS NOT NULL) AS kontlu,
(SELECT COUNT(*) FROM netler) AS net,
(SELECT COUNT(DISTINCT program_id) FROM netler) AS netli_program`
)
.get() as { kontlu: number; net: number; netli_program: number };
const dagilim = db
.prepare("SELECT yil, COUNT(*) AS n FROM netler GROUP BY yil ORDER BY yil")
.all() as { yil: number; n: number }[];
console.log(
`Özet → kontenjan kırılımı olan program: ${ozet.kontlu}, ` +
`net satırı: ${ozet.net} (${ozet.netli_program} program)`
`\nNetler: ${yazilan} satır yazıldı/güncellendi; programı DB'de olmayan: ${programYok}; yıl aralığı dışı: ${yilDisi}`
);
db.close();
console.log(`Netler yıl dağılımı: ${dagilim.map((d) => `${d.yil}=${d.n}`).join(" ")}`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
// ── B) Kontenjan kırılımı ────────────────────────────────────────────────
async function kirilimAdimi(db: Db, yil: number, azamiSayfa: number): Promise<void> {
const veriYili = yil - 1; // `…1` alanları; yıl kilidiyle doğrulanır, r.yil'e bakılmaz
const first = await fetchPage<KirilimKaydi>(KILAVUZ_API, 0);
yilKilidi(db, yil, first.content);
const eksik = KIRILIM_ALANLARI.filter(
([alan]) => !first.content.some((r) => r[alan] !== undefined)
).map(([alan]) => alan);
if (eksik.length) {
dur(
`Kırılım alanları API'de yok (ilk sayfanın tamamında undefined): ${eksik.join(", ")}. ` +
`${veriYili} kırılım kolonlarına DOKUNULMADI (mevcut değerler korunuyor).`
);
}
const kontCols = KIRILIM_ALANLARI.map(([, onek]) => `${onek}${veriYili}`);
const mevcut = new Set(kolonAdlari(db, "programs"));
for (const col of kontCols) {
if (!mevcut.has(col)) db.exec(`ALTER TABLE programs ADD COLUMN ${col} INTEGER`);
}
const updateKont = db.prepare(`
UPDATE programs SET ${kontCols.map((c) => `${c} = COALESCE(?, ${c})`).join(", ")}
WHERE id = ?
`);
const exists = db.prepare("SELECT 1 FROM programs WHERE id = ?");
let guncellenen = 0;
let dbdeYok = 0;
const isle = db.transaction((records: KirilimKaydi[]) => {
for (const r of records) {
if (!r.kilavuzKodu) continue;
const id = String(r.kilavuzKodu);
if (!exists.get(id)) {
dbdeYok++;
continue;
}
updateKont.run(...KIRILIM_ALANLARI.map(([alan]) => num(r[alan])), id);
guncellenen++;
}
});
const sayfa = Math.min(first.totalPages, azamiSayfa);
console.log(`Kontenjan kırılımı (${veriYili}): ${first.totalElements} kayıt, ${sayfa} sayfa…`);
isle(first.content);
for (let p = 1; p < sayfa; p++) {
await sleep(DELAY_MS);
isle((await fetchPage<KirilimKaydi>(KILAVUZ_API, p)).content);
process.stdout.write(`\rkırılım sayfa ${p + 1}/${sayfa}`);
}
console.log(`\nKontenjan kırılımı: ${guncellenen} güncellendi, ${dbdeYok} DB'de yok`);
}
async function main() {
const yil = yilArgumani();
const db = dbAc(dbArgumani());
// Yalnız deneme için: kaynağı yormadan ilk N sayfayla dur.
const azamiSayfa = Number.parseInt(argOku("azami-sayfa") ?? "", 10) || Number.POSITIVE_INFINITY;
let hata: unknown = null;
if (!bayrakVar("netler-atla")) {
try {
await netlerAdimi(db, yil, azamiSayfa);
} catch (err) {
hata = err; // adımlar bağımsız: netler dursa da kırılım (istenmişse) denenir
console.error(`Netler adımı durdu: ${err instanceof Error ? err.message : err}`);
}
}
if (bayrakVar("kirilim")) {
await kirilimAdimi(db, yil, azamiSayfa);
} else {
console.log("Kontenjan kırılımı atlandı (yalnız --kirilim ile koşar).");
}
db.pragma("wal_checkpoint(TRUNCATE)");
db.close();
if (hata) throw hata;
}
if (process.argv[1]?.endsWith("detay.ts")) calistir(main);

228
scripts/eski-arsivle.ts Normal file
View File

@@ -0,0 +1,228 @@
/**
* Güncel tercih kılavuzunda OLMAYAN programları `programs` → `programs_arsiv`
* tablosuna taşır (silmez; aynı şema, aynı değerler). Kodu değişen programların
* çift satırı (eski kod + yeni kod) böylece sorgu koşuluyla değil DB inşasında
* çözülür: site yalnız bu yıl tercih edilebilen programları görür.
*
* `netler.program_id`: arşive giden eski kodun TEK bir güncel karşılığı varsa
* (programs.eski_kod) yeni koda çevrilir; yeni kodda o yıl zaten net satırı
* varsa eski satır olduğu gibi bırakılır (veri silinmez).
*
* ÖNCE `--kuru` ile koş: hiçbir şey yazmaz; kaç satır gideceğini ve hangi bölüm
* / üniversite sayfalarının (slug) tamamen kapanacağını adlarıyla listeler.
* Gerçek koşu [BİLAL] onayı ister — sayfalar siteden kalkar.
*
* Kullanım:
* pnpm eski-arsivle --yil 2026 --db /tmp/deneme.db --kuru
* pnpm eski-arsivle --yil 2026 --db /tmp/deneme.db
*/
import { bolumBazAdi, turkishSlugify, uniSayfaSlug } from "../src/lib/slug";
import {
arsivTablosunuGarantiEt,
bayrakVar,
calistir,
dbAc,
dbArgumani,
dur,
kolonAdlari,
metaOku,
metaYaz,
tabloVar,
yilArgumani,
} from "./yokatlas-goc";
type Ad = { ad: string; n: number };
async function main() {
const yil = yilArgumani();
const dbYolu = dbArgumani();
const kuru = bayrakVar("kuru");
const db = dbAc(dbYolu, { saltOkunur: kuru });
console.log(`DB: ${dbYolu}${kuru ? "\nKURU KİP: hiçbir şey yazılmayacak" : ""}`);
if (!kolonAdlari(db, "programs").includes("son_kilavuz_yili")) {
dur("son_kilavuz_yili kolonu yok: önce refresh koşulmalı.");
}
// ── Emniyet: refresh bu yıl için tam koşmuş olmalı ─────────────────────
const apiToplam = Number(metaOku(db, `refresh_${yil}_api_toplam`) ?? NaN);
const atlanan = Number(metaOku(db, `refresh_${yil}_atlanan`) ?? NaN);
const guncel = (
db.prepare("SELECT COUNT(*) AS n FROM programs WHERE son_kilavuz_yili = ?").get(yil) as { n: number }
).n;
if (!Number.isFinite(apiToplam) || guncel !== apiToplam - atlanan) {
dur(
`Güncel satır sayısı (${guncel}) refresh kaydıyla (API ${apiToplam} − atlanan ${atlanan}) uyuşmuyor. ` +
"Yarım bir refresh'in üstüne arşivleme yapılmaz — güncel programlar da gidebilir."
);
}
// NOT: `<>` NULL'ları kaçırır; son_kilavuz_yili bilinmeyen (NULL) satırlar da eskidir.
const ESKI = "son_kilavuz_yili IS NOT ?";
const eskiSayi = (
db.prepare(`SELECT COUNT(*) AS n FROM programs WHERE ${ESKI}`).get(yil) as { n: number }
).n;
console.log(`\nGüncel (${yil} kılavuzunda): ${guncel} Arşivlenecek: ${eskiSayi}`);
if (eskiSayi === 0) {
console.log("Arşivlenecek satır yok.");
db.close();
return;
}
const kirilim = db
.prepare(
`SELECT COALESCE(CAST(son_kilavuz_yili AS TEXT), 'bilinmiyor (≤2024)') AS y, COUNT(*) AS n,
COUNT(sira${yil - 1}) AS onceki_sirali
FROM programs WHERE ${ESKI} GROUP BY son_kilavuz_yili ORDER BY son_kilavuz_yili`
)
.all(yil) as { y: string; n: number; onceki_sirali: number }[];
for (const k of kirilim) {
console.log(` son kılavuz yılı ${k.y}: ${k.n} satır (${k.onceki_sirali} tanesinde sira${yil - 1} dolu)`);
}
const halefli = (
db
.prepare(
`SELECT COUNT(*) AS n FROM programs e WHERE e.${ESKI}
AND EXISTS (SELECT 1 FROM programs g WHERE g.eski_kod = e.id AND g.son_kilavuz_yili = ?)`
)
.get(yil, yil) as { n: number }
).n;
console.log(
` yeni kodla devam eden (eski_kod eşleşmesi): ${halefli}; karşılığı olmayan (kapanmış/ad değişmiş): ${eskiSayi - halefli}`
);
// ── SEO: tamamen kaybolan bölüm / üniversite sayfaları ─────────────────
const eskiIsimler = db
.prepare(`SELECT isim AS ad, COUNT(*) AS n FROM programs WHERE ${ESKI} GROUP BY isim`)
.all(yil) as Ad[];
const guncelIsimler = db
.prepare("SELECT DISTINCT isim AS ad FROM programs WHERE son_kilavuz_yili = ?")
.all(yil) as { ad: string }[];
const guncelHamIsim = new Set(guncelIsimler.map((r) => r.ad));
const guncelBolumSlug = new Set(guncelIsimler.map((r) => turkishSlugify(bolumBazAdi(r.ad))));
const kaybolanHamIsim = eskiIsimler.filter((r) => !guncelHamIsim.has(r.ad));
const kaybolanBolum = new Map<string, { ad: string; n: number }>();
for (const r of eskiIsimler) {
const baz = bolumBazAdi(r.ad);
const slug = turkishSlugify(baz);
if (!slug || guncelBolumSlug.has(slug)) continue;
const k = kaybolanBolum.get(slug) ?? { ad: baz, n: 0 };
k.n += r.n;
kaybolanBolum.set(slug, k);
}
const eskiUniler = db
.prepare(`SELECT universite AS ad, COUNT(*) AS n FROM programs WHERE ${ESKI} GROUP BY universite`)
.all(yil) as Ad[];
const guncelUniler = db
.prepare("SELECT DISTINCT universite AS ad FROM programs WHERE son_kilavuz_yili = ?")
.all(yil) as { ad: string }[];
const guncelHamUni = new Set(guncelUniler.map((r) => r.ad));
const guncelUniSlug = new Set(guncelUniler.map((r) => uniSayfaSlug(r.ad)));
const kaybolanHamUni = eskiUniler.filter((r) => !guncelHamUni.has(r.ad));
const kaybolanUni = new Map<string, { ad: string; n: number }>();
for (const r of eskiUniler) {
const slug = uniSayfaSlug(r.ad);
if (!slug || guncelUniSlug.has(slug)) continue;
const k = kaybolanUni.get(slug) ?? { ad: r.ad, n: 0 };
k.n += r.n;
kaybolanUni.set(slug, k);
}
console.log(
`\nTamamen kaybolan ham 'isim' değeri: ${kaybolanHamIsim.length} ` +
`(çoğu burs/dil parantezi varyantı; sayfa kaybı aşağıdaki slug listesidir)`
);
console.log(`KAPANACAK /bolum/<slug> sayfası: ${kaybolanBolum.size}`);
for (const [slug, k] of [...kaybolanBolum].sort((a, b) => a[0].localeCompare(b[0]))) {
console.log(` /bolum/${slug} — ${k.ad} (${k.n} satır)`);
}
console.log(`Tamamen kaybolan ham 'universite' değeri: ${kaybolanHamUni.length}`);
for (const r of kaybolanHamUni) console.log(` ${r.ad} (${r.n} satır)`);
console.log(`KAPANACAK /universite/<slug> sayfası: ${kaybolanUni.size}`);
for (const [slug, k] of [...kaybolanUni].sort((a, b) => a[0].localeCompare(b[0]))) {
console.log(` /universite/${slug} — ${k.ad} (${k.n} satır)`);
}
// ── netler ─────────────────────────────────────────────────────────────
const netlerVar = tabloVar(db, "netler");
if (netlerVar) {
const net = db
.prepare(
`SELECT COUNT(*) AS satir, COUNT(DISTINCT n.program_id) AS program
FROM netler n JOIN programs e ON e.id = n.program_id WHERE e.${ESKI}`
)
.get(yil) as { satir: number; program: number };
console.log(`\nnetler: arşive gidecek programlara bağlı ${net.satir} satır (${net.program} program)`);
}
if (kuru) {
console.log("\nKURU KİP bitti — DB değişmedi.");
db.close();
return;
}
// ── Gerçek taşıma: tek transaction ─────────────────────────────────────
db.transaction(() => {
arsivTablosunuGarantiEt(db);
const kolonlar = kolonAdlari(db, "programs");
const saglama = () =>
(
db
.prepare(
`SELECT COUNT(*) || '|' || TOTAL(sira${yil - 1}) || '|' || TOTAL(puan${yil - 1}) AS s
FROM (SELECT * FROM programs UNION ALL SELECT ${kolonlar.join(", ")} FROM programs_arsiv)`
)
.get() as { s: string }
).s;
const once = saglama();
if (netlerVar) {
// Tek halefli eski kodlar → yeni koda. OR IGNORE: yeni kodda o yıl varsa dokunma.
const r = db
.prepare(
`UPDATE OR IGNORE netler SET program_id = (
SELECT g.id FROM programs g WHERE g.eski_kod = netler.program_id AND g.son_kilavuz_yili = @yil
)
WHERE program_id IN (SELECT id FROM programs WHERE son_kilavuz_yili IS NOT @yil)
AND (SELECT COUNT(*) FROM programs g
WHERE g.eski_kod = netler.program_id AND g.son_kilavuz_yili = @yil) = 1`
)
.run({ yil });
console.log(`\nnetler: ${r.changes} satır yeni koda çevrildi`);
}
const tasinan = db
.prepare(
`INSERT INTO programs_arsiv (${kolonlar.join(", ")})
SELECT ${kolonlar.join(", ")} FROM programs WHERE ${ESKI}`
)
.run(yil).changes;
const cikan = db.prepare(`DELETE FROM programs WHERE ${ESKI}`).run(yil).changes;
if (tasinan !== eskiSayi || cikan !== eskiSayi) {
dur(`Taşınan (${tasinan}) / çıkarılan (${cikan}) ≠ beklenen (${eskiSayi}). Geri alındı.`);
}
const sonra = saglama();
if (once !== sonra) {
dur(`programs ∪ programs_arsiv sağlaması değişti. Geri alındı.\nönce : ${once}\nsonra: ${sonra}`);
}
metaYaz(db, `arsiv_${yil}_tarih`, new Date().toISOString());
metaYaz(db, `arsiv_${yil}_satir`, tasinan);
console.log(`Arşivlendi: ${tasinan} satır → programs_arsiv. Sağlama aynı: ${sonra}`);
})();
if (netlerVar) {
const yetim = (
db
.prepare("SELECT COUNT(*) AS n FROM netler WHERE program_id NOT IN (SELECT id FROM programs)")
.get() as { n: number }
).n;
console.log(`netler: programs'ta karşılığı olmayan satır (arşivdeki programlara ait): ${yetim}`);
}
db.pragma("wal_checkpoint(TRUNCATE)");
db.close();
console.log("\nSıradaki adım: pnpm db:index --db <db> --vacuum sonra pnpm veri-kalite …");
}
if (process.argv[1]?.endsWith("eski-arsivle.ts")) calistir(main);

302
scripts/gecmis-doldur.ts Normal file
View File

@@ -0,0 +1,302 @@
/**
* Güncel kılavuzdaki programların GEÇMİŞ yıl kolonlarındaki boşlukları doldurur.
*
* Kurallar (pazarlıksız):
* - Yalnız NULL alan doldurulur. Dolu değer ASLA ezilmez; kaynak farklı bir
* değer söylüyorsa sayılır ve raporlanır (karar insanındır).
* - Tahmin yok, komşu yıldan kopya yok. Kaynak yoksa alan NULL kalır.
* - Belirsiz eşleşmede birleştirme yok (sayılır, raporlanır).
*
* Kaynak önceliği:
* 1. API `…1/…2/…3` alanları (ham dosyadan; API'ye gidilmez) → yıl-1/-2/-3
* için sıra, puan, genel kontenjan.
* 2. `eski_kod` (API `eskiKilavuzKodu`): kodu değişen programın eski satırı
* (programs ya da programs_arsiv) → tüm geçmiş yıllar, dört ölçü.
* Eski kod hâlâ güncel kılavuzdaysa ya da birden çok yeni program aynı
* eski koda işaret ediyorsa (bölünme) → belirsiz, atlanır.
* 3. Aynı (üniversite baz adı, isim, tur, onlisans) için TAM 1 güncel + TAM 1
* eski satır varsa; ortak yıllarda puan/kontenjan/yerleşen ÇELİŞMİYORSA ve
* en az bir ortak değer AYNIYSA (kanıt) → eski satırdan doldurulur, güncel
* satıra `eski_kod` yazılır. Kanıtsız ya da çelişkili eşleşme birleştirilmez.
*
* Kullanım:
* pnpm gecmis-doldur --yil 2026 --db /tmp/deneme.db [--ham data/ham/2026-….jsonl.gz] [--kuru]
*/
import path from "node:path";
import type Database from "better-sqlite3";
import { ILK_YIL } from "../src/lib/veri-yillari";
import { gecmisAlanlari, hamOku, sonHamDosya } from "./refresh";
import {
argOku,
bayrakVar,
calistir,
dbAc,
dbArgumani,
dur,
kolonAdlari,
tabloVar,
yilArgumani,
YIL_OLCULERI,
} from "./yokatlas-goc";
type Satir = Record<string, string | number | null> & { id: string };
const PUAN_TOLERANS = 0.01;
class KuruGeriAl extends Error {}
function ayniMi(olcu: string, a: number, b: number): boolean {
return olcu === "puan" ? Math.abs(a - b) <= PUAN_TOLERANS : a === b;
}
async function main() {
const yil = yilArgumani();
const dbYolu = dbArgumani();
const kuru = bayrakVar("kuru");
const hamArg = argOku("ham");
const hamDosya = hamArg
? path.resolve(process.cwd(), hamArg)
: sonHamDosya(yil, path.resolve(process.cwd(), "data", "ham"));
if (!hamDosya) {
dur(`--ham verilmedi ve data/ham altında ${yil}-*.jsonl.gz yok. Önce refresh koş.`);
}
const db = dbAc(dbYolu);
const kolonlar = new Set(kolonAdlari(db, "programs"));
if (!kolonlar.has("son_kilavuz_yili") || !kolonlar.has(`sira${yil}`)) {
dur("Şema eski: önce `pnpm refresh` (ya da `pnpm yokatlas:goc`) koşulmalı.");
}
const guncelSayisi = (
db.prepare("SELECT COUNT(*) AS n FROM programs WHERE son_kilavuz_yili = ?").get(yil) as { n: number }
).n;
if (guncelSayisi === 0) dur(`son_kilavuz_yili=${yil} satır yok: önce refresh koşulmalı.`);
const gecmisYillar: number[] = [];
for (let y = ILK_YIL; y < yil; y++) {
if (kolonlar.has(`sira${y}`)) gecmisYillar.push(y);
}
const olculer = YIL_OLCULERI.map(([o]) => o);
console.log(`DB: ${dbYolu}\nHam: ${hamDosya}${kuru ? "\nKURU KİP: hiçbir şey yazılmayacak" : ""}`);
const { kayitlar } = await hamOku(hamDosya, yil);
const doldurulan = new Map<string, number>(); // "kaynak|kolon" → adet
const farkli = new Map<string, { adet: number; ornek: string[] }>();
const say = (m: Map<string, number>, k: string) => m.set(k, (m.get(k) ?? 0) + 1);
const farkYaz = (kaynak: string, kolon: string, id: string, db_: number, kaynakDeger: number) => {
const k = `${kaynak}|${kolon}`;
const f = farkli.get(k) ?? { adet: 0, ornek: [] };
f.adet++;
if (f.ornek.length < 3) f.ornek.push(`${id}: DB=${db_} kaynak=${kaynakDeger}`);
farkli.set(k, f);
};
const satirGetir = db.prepare("SELECT * FROM programs WHERE id = ?");
const arsivGetir = tabloVar(db, "programs_arsiv")
? db.prepare("SELECT * FROM programs_arsiv WHERE id = ?")
: null;
const setCache = new Map<string, Database.Statement<unknown[]>>();
const alanYaz = (id: string, kolon: string, deger: number) => {
let st = setCache.get(kolon);
if (!st) {
// Çifte emniyet: WHERE … IS NULL — dolu değerin üstüne yazılamaz.
st = db.prepare(`UPDATE programs SET ${kolon} = ? WHERE id = ? AND ${kolon} IS NULL`);
setCache.set(kolon, st);
}
st.run(deger, id);
};
/** Hedefin NULL alanlarını kaynaktan doldurur; dolu+farklıyı sayar. */
const satirdanDoldur = (kaynakAdi: string, hedef: Satir, kaynak: Satir) => {
for (const y of gecmisYillar) {
for (const olcu of olculer) {
const kolon = `${olcu}${y}`;
const k = kaynak[kolon] as number | null;
if (k == null) continue;
const h = hedef[kolon] as number | null;
if (h == null) {
alanYaz(hedef.id, kolon, k);
hedef[kolon] = k;
say(doldurulan, `${kaynakAdi}|${kolon}`);
} else if (!ayniMi(olcu, h, k)) {
farkYaz(kaynakAdi, kolon, hedef.id, h, k);
}
}
}
};
const ozet = {
apiEslesen: 0,
eskiKodlu: 0,
eskiKodBulunamadi: 0,
eskiKodHalaGuncel: 0,
eskiKodBolunme: 0,
adEslesen: 0,
adBelirsiz: 0,
adPuanCelisik: 0,
adKanitsiz: 0,
};
const ornekler: Record<string, string[]> = { bolunme: [], halaGuncel: [], adEslesen: [], adCelisik: [], adKanitsiz: [] };
const ornekEkle = (k: string, s: string) => {
if (ornekler[k].length < 3) ornekler[k].push(s);
};
try {
db.transaction(() => {
// ── 1. API …1/…2/…3 ────────────────────────────────────────────────
for (const r of kayitlar) {
if (!r.kilavuzKodu) continue;
const hedef = satirGetir.get(String(r.kilavuzKodu)) as Satir | undefined;
if (!hedef || hedef.son_kilavuz_yili !== yil) continue;
ozet.apiEslesen++;
for (const g of gecmisAlanlari(r)) {
const y = yil - g.fark;
if (!gecmisYillar.includes(y)) continue;
for (const olcu of ["sira", "puan", "kontenjan"] as const) {
const k = g[olcu];
if (k == null) continue;
const kolon = `${olcu}${y}`;
const h = hedef[kolon] as number | null;
if (h == null) {
alanYaz(hedef.id, kolon, k);
hedef[kolon] = k;
say(doldurulan, `1-api|${kolon}`);
} else if (!ayniMi(olcu, h, k)) {
farkYaz("1-api", kolon, hedef.id, h, k);
}
}
}
}
// ── 2. eski_kod ────────────────────────────────────────────────────
const eskiKodlular = db
.prepare("SELECT * FROM programs WHERE son_kilavuz_yili = ? AND eski_kod IS NOT NULL")
.all(yil) as Satir[];
const eskiKullanim = new Map<string, number>();
for (const s of eskiKodlular) say(eskiKullanim, s.eski_kod as string);
for (const hedef of eskiKodlular) {
const eskiId = hedef.eski_kod as string;
if ((eskiKullanim.get(eskiId) ?? 0) > 1) {
ozet.eskiKodBolunme++;
ornekEkle("bolunme", `${hedef.id} ← ${eskiId} (${hedef.isim})`);
continue;
}
const kaynak =
(satirGetir.get(eskiId) as Satir | undefined) ??
(arsivGetir?.get(eskiId) as Satir | undefined);
if (!kaynak) {
ozet.eskiKodBulunamadi++;
continue;
}
if (kaynak.son_kilavuz_yili === yil) {
ozet.eskiKodHalaGuncel++;
ornekEkle("halaGuncel", `${hedef.id} ← ${eskiId} (${kaynak.isim})`);
continue;
}
ozet.eskiKodlu++;
satirdanDoldur("2-eski_kod", hedef, kaynak);
}
// ── 3. Benzersiz (universite, isim, tur, onlisans) ─────────────────
const sahiplenilmis = new Set(eskiKullanim.keys());
const gruplar = new Map<string, { guncel: Satir[]; eski: Satir[] }>();
for (const s of db.prepare("SELECT * FROM programs").all() as Satir[]) {
// API 2026'da üniversite adına "(İL)" eki koyuyor; eski satırlar eksiz.
const uniBaz = String(s.universite).replace(/\s*\([^)]*\)\s*$/, "").trim();
const anahtar = JSON.stringify([uniBaz, s.isim, s.tur, s.onlisans]);
const g = gruplar.get(anahtar) ?? { guncel: [], eski: [] };
if (s.son_kilavuz_yili === yil) g.guncel.push(s);
else if (!sahiplenilmis.has(s.id)) g.eski.push(s);
gruplar.set(anahtar, g);
}
const eskiKodYaz = db.prepare(
"UPDATE programs SET eski_kod = ? WHERE id = ? AND eski_kod IS NULL"
);
for (const g of gruplar.values()) {
if (g.eski.length === 0 || g.guncel.length === 0) continue;
if (g.eski.length !== 1 || g.guncel.length !== 1) {
ozet.adBelirsiz++;
continue;
}
const [hedef] = g.guncel;
const [kaynak] = g.eski;
if (hedef.eski_kod != null) {
// API başka bir eski kod söylüyor; ad eşleşmesi onun önüne geçmez.
ozet.adBelirsiz++;
continue;
}
// Aynı yılda iki satırda da veri varsa değerler AYNI olmalı (çift satır);
// farklıysa bunlar aynı anda var olmuş iki ayrı programdır (ör. 1 kişilik
// özel kontenjanlı adaş program). Sıra bilerek dışarıda: 2024 sırası iki
// kaynakta farklı tanımlı. Ortak yılı hiç olmayan eşleşme KANITSIZDIR →
// birleştirilmez, yalnız sayılır.
let kanit = 0;
let celisik = false;
for (const y of gecmisYillar) {
for (const olcu of ["puan", "kontenjan", "yerlesen"]) {
const a = hedef[`${olcu}${y}`] as number | null;
const b = kaynak[`${olcu}${y}`] as number | null;
if (a == null || b == null) continue;
if (ayniMi(olcu, a, b)) kanit++;
else celisik = true;
}
}
if (celisik) {
ozet.adPuanCelisik++;
ornekEkle("adCelisik", `${hedef.id} ~ ${kaynak.id} (${hedef.universite} / ${hedef.isim})`);
continue;
}
if (kanit === 0) {
ozet.adKanitsiz++;
ornekEkle("adKanitsiz", `${hedef.id} ? ${kaynak.id} (${hedef.universite} / ${hedef.isim})`);
continue;
}
ozet.adEslesen++;
ornekEkle("adEslesen", `${hedef.id} ← ${kaynak.id} (${hedef.universite} / ${hedef.isim})`);
satirdanDoldur("3-ad", hedef, kaynak);
eskiKodYaz.run(kaynak.id, hedef.id);
}
if (kuru) throw new KuruGeriAl();
})();
} catch (err) {
if (!(err instanceof KuruGeriAl)) throw err;
}
// ── Rapor ──────────────────────────────────────────────────────────────
console.log(`\nGüncel kılavuzdaki program: ${guncelSayisi}; API kaydıyla eşleşen: ${ozet.apiEslesen}`);
console.log(
`eski_kod: ${ozet.eskiKodlu} programın eski satırı bulundu ve boşlukları ondan dolduruldu; ` +
`eski satır yok: ${ozet.eskiKodBulunamadi}; eski kod hâlâ güncel (atlandı): ${ozet.eskiKodHalaGuncel}; ` +
`bölünme/çoklu (atlandı): ${ozet.eskiKodBolunme}`
);
console.log(
`Ad eşleşmesi: ${ozet.adEslesen} birebir; belirsiz (atlandı): ${ozet.adBelirsiz}; ` +
`aynı yılda farklı değer = ayrı program (atlandı): ${ozet.adPuanCelisik}; ` +
`ortak yılı olmayan, kanıtsız (atlandı): ${ozet.adKanitsiz}`
);
for (const [k, v] of Object.entries(ornekler)) {
if (v.length) console.log(` örnek ${k}: ${v.join(" ; ")}`);
}
console.log(`\nDoldurulan NULL alanlar${kuru ? " (kuru — yazılmadı)" : ""}:`);
const toplamlar = new Map<string, number>();
for (const [k, n] of [...doldurulan].sort()) {
const [kaynak] = k.split("|");
toplamlar.set(kaynak, (toplamlar.get(kaynak) ?? 0) + n);
console.log(` ${k.replace("|", " ")}: ${n}`);
}
for (const [kaynak, n] of toplamlar) console.log(` TOPLAM ${kaynak}: ${n}`);
if (doldurulan.size === 0) console.log(" (yok)");
console.log("\nDolu olup kaynağın FARKLI söylediği alanlar (EZİLMEDİ):");
for (const [k, f] of [...farkli].sort()) {
console.log(` ${k.replace("|", " ")}: ${f.adet} ör. ${f.ornek.join(" ; ")}`);
}
if (farkli.size === 0) console.log(" (yok)");
if (!kuru) db.pragma("wal_checkpoint(TRUNCATE)");
db.close();
}
if (process.argv[1]?.endsWith("gecmis-doldur.ts")) calistir(main);

View File

@@ -1,24 +1,66 @@
/**
* Canlı YÖK Atlas API'sinden 2025 yerleştirme verisini çekip DB'yi tazeler.
* Canlı YÖK Atlas API'sinden SON yerleştirme yılının verisini çekip DB'ye işler.
*
* Endpoint: POST https://yokatlas.yok.gov.tr/api/tercih-kilavuz/search
* (yokatlas-py'nin kullandığı resmî tercih-kılavuz JSON API'si)
*
* - Mevcut programlarda sira/puan/kontenjan/yerlesen 2025 kolonlarını günceller
* - DB'de olmayan (yeni açılan) programları ekler
* - Az yerleşeni olan programlarda API basariSirasi döndürmez → NULL kalır
* API'nin yıl düzeni (Eylül 2026'da doğrulandı): EKSİZ alanlar (basariSirasi,
* minPuan, kontenjan, gkY…) en son yerleştirme yılıdır; `…1` bir önceki,
* `…2` iki önceki, `…3` üç önceki yıldır. Kayıttaki `yil` alanı GÜVENİLMEZ
* (kılavuz yılı / veri yılı anlamı yıl içinde değişiyor) — bu betik yılı
* HİÇBİR ZAMAN oradan türetmez. Yıl `--yil` ile verilir ve YIL KİLİDİ ile
* DB'ye karşı doğrulanır.
*
* Kullanım: npx tsx scripts/refresh.ts
* Güvenlik sırası:
* 1. `--yil` = SON_YIL, `--db` zorunlu (varsayılan yok).
* 2. YIL KİLİDİ: ilk sayfadaki `basariSirasi1` değerleri DB'deki
* sira<yil-1> ile ≥%95 eşleşmeli VE eksiz `basariSirasi` ondan farklı
* olmalı. Değilse API henüz yeni yıla dönmemiştir (ya da bir yıl daha
* ilerlemiştir) → HİÇBİR ŞEY yazılmadan durur.
* 3. Tüm sayfalar önce belleğe + ham dosyaya alınır; benzersiz kod sayısı =
* `totalElements` değilse (sayfalama kayması) durur.
* 4. DB yazımı TEK transaction; önceki yılların kolon sağlaması transaction
* içinde karşılaştırılır, fark varsa geri alınır.
*
* Yazdıkları: sira/puan/kontenjan/yerlesen<yil>, son_kilavuz_yili, eski_kod.
* Mevcut satırların ÖNCEKİ yıl kolonlarına dokunmaz (onları yalnız NULL ise
* gecmis-doldur.ts doldurur). Yeni program eklerken `…1/…2/…3` alanlarından
* önceki üç yılı da yazar. API "veri yok" için 0 döndürür → NULL.
*
* Kaynağa nazik: sayfa başı 500 kayıt, istekler arası 0,5 sn, tek bağlantı.
* `--ham <dosya>` verilirse API'ye HİÇ gidilmez; daha önce kaydedilmiş ham
* dosyadan aynı işlem tekrarlanır (kopyada deneme → gerçek DB'ye uygulama).
*
* Kullanım:
* pnpm refresh --yil 2026 --db /tmp/deneme.db
* pnpm refresh --yil 2026 --db data/yokatlas.db --ham data/ham/2026-2026-09-21.jsonl.gz
*/
import fs from "node:fs";
import path from "node:path";
import Database from "better-sqlite3";
import readline from "node:readline";
import zlib from "node:zlib";
import { ILK_YIL } from "../src/lib/veri-yillari";
import { efektifIndeksiKur } from "./db-index";
import { uniAdiNormalize, uniturOnar } from "./unitur-onar";
import {
argOku,
calistir,
dbAc,
dbArgumani,
dur,
metaYaz,
yilArgumani,
yilKolonlariniGarantiEt,
yokatlasGoc,
type Db,
} from "./yokatlas-goc";
const API = "https://yokatlas.yok.gov.tr/api/tercih-kilavuz/search";
const PAGE_SIZE = 500;
const DELAY_MS = 300;
const DELAY_MS = 500;
const KILIT_ESIK = 0.95;
const KILIT_ASGARI_ORNEK = 50;
const TUR_MAP: Record<string, string> = {
export const TUR_MAP: Record<string, string> = {
SAY: "SAYISAL",
EA: "EŞİT AĞIRLIK",
SÖZ: "SÖZEL",
@@ -26,27 +68,75 @@ const TUR_MAP: Record<string, string> = {
TYT: "TYT",
};
type ApiRecord = {
/** API kaydından betiklerin kullandığı alanlar (ham dosyada kaydın tamamı durur). */
export type KilavuzKaydi = {
kilavuzKodu: number;
eskiKilavuzKodu?: number | null;
birimAdi: string;
universiteAdi: string;
universiteTuru: string | null;
ilAdi: string | null;
fymkAdi: string | null;
universiteTuru?: string | null;
ilAdi?: string | null;
fymkAdi?: string | null;
puanTuru: string;
ogrenimSuresi: number | null;
ogrenimSuresi?: number | null;
birimTuruId: number;
basariSirasi?: number | null;
minPuan?: number | null;
basariSirasi?: number | string | null;
minPuan?: number | string | null;
kontenjan?: number | null;
gkY?: number | null;
sgyY?: number | null;
dprmY?: number | null;
obkY?: number | null;
basariSirasi1?: number | string | null;
basariSirasi2?: number | string | null;
basariSirasi3?: number | string | null;
minPuan1?: number | string | null;
minPuan2?: number | string | null;
minPuan3?: number | string | null;
gk1?: number | null;
gk2?: number | null;
gk3?: number | null;
};
const TUTULAN_ALANLAR: (keyof KilavuzKaydi)[] = [
"kilavuzKodu",
"eskiKilavuzKodu",
"birimAdi",
"universiteAdi",
"universiteTuru",
"ilAdi",
"fymkAdi",
"puanTuru",
"ogrenimSuresi",
"birimTuruId",
"basariSirasi",
"minPuan",
"kontenjan",
"gkY",
"sgyY",
"dprmY",
"obkY",
"basariSirasi1",
"basariSirasi2",
"basariSirasi3",
"minPuan1",
"minPuan2",
"minPuan3",
"gk1",
"gk2",
"gk3",
];
function inceKayit(ham: Record<string, unknown>): KilavuzKaydi {
const ince: Record<string, unknown> = {};
for (const alan of TUTULAN_ALANLAR) {
if (ham[alan] !== undefined) ince[alan] = ham[alan];
}
return ince as KilavuzKaydi;
}
type ApiPage = {
content: ApiRecord[];
content: Record<string, unknown>[];
totalPages: number;
totalElements: number;
};
@@ -83,140 +173,435 @@ async function fetchPage(page: number): Promise<ApiPage> {
return (await res.json()) as ApiPage;
}
function yerlesen(r: ApiRecord): number | null {
const parts = [r.gkY, r.sgyY, r.dprmY, r.obkY].filter(
(x): x is number => typeof x === "number"
);
return parts.length ? parts.reduce((a, b) => a + b, 0) : null;
}
function intOrNull(v: unknown): number | null {
/** Pozitif tam sayı; 0 / boş / sayı değil → NULL (API "veri yok"u 0 ile verir). */
export function intOrNull(v: unknown): number | null {
const n = typeof v === "string" ? Number.parseFloat(v) : (v as number);
return typeof n === "number" && Number.isFinite(n) && n > 0
? Math.round(n)
: null;
}
function realOrNull(v: unknown): number | null {
export function realOrNull(v: unknown): number | null {
const n = typeof v === "string" ? Number.parseFloat(v) : (v as number);
return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : null;
}
async function main() {
const db = new Database(path.join(process.cwd(), "data", "yokatlas.db"));
db.pragma("journal_mode = WAL");
function yerlesen(r: KilavuzKaydi): number | null {
const parts = [r.gkY, r.sgyY, r.dprmY, r.obkY].filter(
(x): x is number => typeof x === "number"
);
return parts.length ? parts.reduce((a, b) => a + b, 0) : null;
}
const cols = db.prepare("PRAGMA table_info(programs)").all() as {
name: string;
}[];
const has2025 = cols.some((c) => c.name === "sira2025");
if (!has2025) {
db.exec(`
ALTER TABLE programs ADD COLUMN sira2025 INTEGER;
ALTER TABLE programs ADD COLUMN puan2025 REAL;
ALTER TABLE programs ADD COLUMN kontenjan2025 INTEGER;
ALTER TABLE programs ADD COLUMN yerlesen2025 INTEGER;
CREATE INDEX IF NOT EXISTS idx_programs_tur_sira25 ON programs (tur, sira2025);
`);
/**
* API `eskiKilavuzKodu` iki anlamda geliyor: gerçek eski program kodu (9 hane)
* ya da yeni açılan programda üniversite öneki (4 hane, ör. 2031). Yalnız
* gerçek program kodu biçimindeki ve yeni koddan farklı olanı döndürür.
*/
export function eskiKod(r: KilavuzKaydi): string | null {
if (!r.eskiKilavuzKodu) return null;
const eski = String(r.eskiKilavuzKodu);
const yeni = String(r.kilavuzKodu);
return eski.length === yeni.length && eski !== yeni ? eski : null;
}
/** Yeni programda önceki üç yılın API karşılığı: [yıl farkı, sıra, puan, genel kontenjan]. */
export function gecmisAlanlari(
r: KilavuzKaydi
): { fark: number; sira: number | null; puan: number | null; kontenjan: number | null }[] {
return [
{ fark: 1, sira: intOrNull(r.basariSirasi1), puan: realOrNull(r.minPuan1), kontenjan: intOrNull(r.gk1) },
{ fark: 2, sira: intOrNull(r.basariSirasi2), puan: realOrNull(r.minPuan2), kontenjan: intOrNull(r.gk2) },
{ fark: 3, sira: intOrNull(r.basariSirasi3), puan: realOrNull(r.minPuan3), kontenjan: intOrNull(r.gk3) },
];
}
// ── Ham dosya ────────────────────────────────────────────────────────────
/** `--ham` verilmemişse o yılın en yeni ham dosyası (ad sırası = tarih sırası). */
export function sonHamDosya(yil: number, dizin: string): string | null {
if (!fs.existsSync(dizin)) return null;
const adaylar = fs
.readdirSync(dizin)
.filter((f) => f.startsWith(`${yil}-`) && f.endsWith(".jsonl.gz"))
.sort();
return adaylar.length ? path.join(dizin, adaylar[adaylar.length - 1]) : null;
}
export type HamOkuma = { toplam: number; kayitlar: KilavuzKaydi[] };
/** Ham .jsonl.gz → ince kayıtlar. İlk satır `_meta` (yıl doğrulanır). */
export async function hamOku(dosya: string, yil: number): Promise<HamOkuma> {
if (!fs.existsSync(dosya)) dur(`Ham dosya yok: ${dosya}`);
const rl = readline.createInterface({
input: fs.createReadStream(dosya).pipe(zlib.createGunzip()),
crlfDelay: Infinity,
});
let toplam = -1;
const kayitlar: KilavuzKaydi[] = [];
for await (const satir of rl) {
if (!satir) continue;
const obj = JSON.parse(satir) as Record<string, unknown>;
if (obj._meta) {
const meta = obj._meta as { yil: number; totalElements: number };
if (meta.yil !== yil) {
dur(`Ham dosya ${meta.yil} yılı için çekilmiş, --yil ${yil} verildi.`);
}
toplam = meta.totalElements;
continue;
}
kayitlar.push(inceKayit(obj));
}
if (toplam < 0) dur(`Ham dosyada _meta satırı yok: ${dosya}`);
return { toplam, kayitlar };
}
// ── Yıl kilidi ───────────────────────────────────────────────────────────
/**
* `basariSirasi1` = DB sira<yil-1> (≥%95) VE eksiz `basariSirasi` ≠ DB
* sira<yil-1>. İkisi birlikte "eksiz alanlar gerçekten <yil> verisi" demektir.
*/
export function yilKilidi(db: Db, yil: number, ornek: KilavuzKaydi[]): void {
const onceki = yil - 1;
const bul = db.prepare(`SELECT sira${onceki} AS s FROM programs WHERE id = ?`);
let kiyas1 = 0;
let esit1 = 0;
let kiyas0 = 0;
let esit0 = 0;
for (const r of ornek) {
if (!r.kilavuzKodu) continue;
let satir = bul.get(String(r.kilavuzKodu)) as { s: number | null } | undefined;
const eski = eskiKod(r);
if (!satir && eski) satir = bul.get(eski) as typeof satir;
if (!satir || satir.s == null) continue;
const s1 = intOrNull(r.basariSirasi1);
if (s1 != null) {
kiyas1++;
if (s1 === satir.s) esit1++;
}
const s0 = intOrNull(r.basariSirasi);
if (s0 != null) {
kiyas0++;
if (s0 === satir.s) esit0++;
}
}
const oran1 = kiyas1 ? esit1 / kiyas1 : 0;
const oran0 = kiyas0 ? esit0 / kiyas0 : 0;
console.log(
`Yıl kilidi: basariSirasi1 = sira${onceki} → ${esit1}/${kiyas1} (%${(oran1 * 100).toFixed(1)}); ` +
`eksiz basariSirasi = sira${onceki} → ${esit0}/${kiyas0} (%${(oran0 * 100).toFixed(1)})`
);
if (kiyas1 < KILIT_ASGARI_ORNEK) {
dur(
`Yıl kilidi için yeterli karşılaştırma yok (${kiyas1} < ${KILIT_ASGARI_ORNEK}). ` +
`DB'de sira${onceki} dolu mu, doğru DB mi?`
);
}
if (oran0 >= 0.5) {
dur(
`API'nin eksiz alanları hâlâ ${onceki} verisi (DB ile %${(oran0 * 100).toFixed(1)} aynı). ` +
`${yil} yerleştirme sonuçları henüz yayımlanmamış; bugün koşmak ${yil} kolonuna ${onceki} verisi yazar.`
);
}
if (oran1 < KILIT_ESIK) {
dur(
`API'nin "…1" alanları DB'deki ${onceki} verisiyle eşleşmiyor (%${(oran1 * 100).toFixed(1)} < %95). ` +
`API yıl düzeni değişmiş olabilir; --yil ${yil} ile yazmak yanlış kolonu doldurur.`
);
}
}
// ── Ana akış ─────────────────────────────────────────────────────────────
async function apidenCek(
yil: number,
hamDizin: string,
ilkSayfaKontrol: (ornek: KilavuzKaydi[]) => void
): Promise<HamOkuma & { hamDosya: string }> {
fs.mkdirSync(hamDizin, { recursive: true });
const tarih = new Date().toISOString().slice(0, 10);
const hamDosya = path.join(hamDizin, `${yil}-${tarih}.jsonl.gz`);
const gecici = `${hamDosya}.tmp`;
const first = await fetchPage(0);
const kayitlar = first.content.map(inceKayit);
// Kilit tutmazsa hiçbir şey yazılmadan ve kaynağa 1 istekle durulur.
ilkSayfaKontrol(kayitlar);
console.log(
`Toplam ${first.totalElements} kayıt, ${first.totalPages} sayfa çekilecek ` +
`(~${Math.ceil((first.totalPages * (DELAY_MS + 1500)) / 1000)} sn)…`
);
const gzip = zlib.createGzip();
const cikti = fs.createWriteStream(gecici);
gzip.pipe(cikti);
const bitti = new Promise<void>((resolve, reject) => {
cikti.on("finish", resolve);
cikti.on("error", reject);
gzip.on("error", reject);
});
const yaz = (obj: unknown) => gzip.write(`${JSON.stringify(obj)}\n`);
yaz({
_meta: {
yil,
cekimTarihi: new Date().toISOString(),
kaynak: API,
totalElements: first.totalElements,
totalPages: first.totalPages,
sayfaBoyu: PAGE_SIZE,
},
});
for (const r of first.content) yaz(r);
for (let p = 1; p < first.totalPages; p++) {
await sleep(DELAY_MS);
const page = await fetchPage(p);
for (const r of page.content) {
yaz(r);
kayitlar.push(inceKayit(r));
}
process.stdout.write(`\rsayfa ${p + 1}/${first.totalPages}`);
}
console.log();
gzip.end();
await bitti;
fs.renameSync(gecici, hamDosya);
console.log(`Ham veri: ${hamDosya}`);
return { toplam: first.totalElements, kayitlar, hamDosya };
}
function oncekiYilSaglamasi(db: Db, yil: number, azamiRowid: number): string {
const kolonlar: string[] = [];
for (let y = ILK_YIL; y < yil; y++) {
for (const olcu of ["sira", "puan", "kontenjan", "yerlesen"]) {
kolonlar.push(`COUNT(${olcu}${y})`, `TOTAL(${olcu}${y})`);
}
}
const r = db
.prepare(`SELECT COUNT(*), ${kolonlar.join(", ")} FROM programs WHERE rowid <= ?`)
.raw()
.get(azamiRowid) as number[];
return r.join("|");
}
async function main() {
const yil = yilArgumani();
const dbYolu = dbArgumani();
const hamArg = argOku("ham");
const hamDizin = path.resolve(process.cwd(), argOku("ham-dizin") ?? path.join("data", "ham"));
const db = dbAc(dbYolu);
console.log(`DB: ${dbYolu}\nYıl: ${yil}`);
// Şema (idempotent). Yeni programlarda …1/…2/…3 yazılacağı için önceki üç
// yılın kolonları da garanti edilir (normalde zaten vardır).
const eklenen = yokatlasGoc(db, yil);
for (let fark = 1; fark <= 3; fark++) {
if (yil - fark >= ILK_YIL) eklenen.push(...yilKolonlariniGarantiEt(db, yil - fark));
}
if (eklenen.length) console.log(`Göç: ${eklenen.join(", ")}`);
// ── Veri: API ya da ham dosya ──────────────────────────────────────────
let toplam: number;
let kayitlar: KilavuzKaydi[];
let hamDosya: string;
if (hamArg) {
hamDosya = path.resolve(process.cwd(), hamArg);
console.log(`Kaynak: ham dosya (API'ye gidilmiyor) → ${hamDosya}`);
({ toplam, kayitlar } = await hamOku(hamDosya, yil));
yilKilidi(db, yil, kayitlar.slice(0, PAGE_SIZE));
} else {
({ toplam, kayitlar, hamDosya } = await apidenCek(yil, hamDizin, (ornek) =>
yilKilidi(db, yil, ornek)
));
}
// Sorgu yolunun kullandığı ifade indexi; ifade src/lib/db.ts EFEKTIF_SIRA
// ile birebir aynı olmalı (bkz. scripts/db-index.ts). sira2025 kolonu
// yukarıda garanti edildikten sonra kurulur.
db.exec(`
CREATE INDEX IF NOT EXISTS idx_programs_tur_onlisans_efektif
ON programs (tur, onlisans, COALESCE(sira2025, sira2024));
`);
// ── Bütünlük: benzersiz kod = totalElements ────────────────────────────
const benzersiz = new Map<string, KilavuzKaydi>();
let kodsuz = 0;
for (const r of kayitlar) {
if (!r.kilavuzKodu) {
kodsuz++;
continue;
}
benzersiz.set(String(r.kilavuzKodu), r);
}
console.log(
`Çekilen: ${kayitlar.length} kayıt, benzersiz kod: ${benzersiz.size}, kodsuz: ${kodsuz}, API toplamı: ${toplam}`
);
if (benzersiz.size + kodsuz !== toplam || kayitlar.length !== toplam) {
dur(
`Benzersiz kod sayısı (${benzersiz.size}) + kodsuz (${kodsuz}) ≠ API toplamı (${toplam}). ` +
"Sayfalama sırasında kayıt kaymış/yinelenmiş; DB'ye HİÇBİR ŞEY yazılmadı. Tekrar dene."
);
}
// ── Yazım: tek transaction ─────────────────────────────────────────────
const update = db.prepare(`
UPDATE programs SET sira2025 = ?, puan2025 = ?, kontenjan2025 = ?, yerlesen2025 = ?
UPDATE programs
SET sira${yil} = ?, puan${yil} = ?, kontenjan${yil} = ?, yerlesen${yil} = ?,
son_kilavuz_yili = ?, eski_kod = COALESCE(?, eski_kod)
WHERE id = ?
`);
const gecmisYillar = [1, 2, 3].filter((f) => yil - f >= ILK_YIL);
const insert = db.prepare(`
INSERT INTO programs (id, isim, universite, unitur, il, fakulte, tur, sure, onlisans,
sira2025, puan2025, kontenjan2025, yerlesen2025)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
sira${yil}, puan${yil}, kontenjan${yil}, yerlesen${yil},
son_kilavuz_yili, eski_kod
${gecmisYillar.map((f) => `, sira${yil - f}, puan${yil - f}, kontenjan${yil - f}`).join("")})
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?${gecmisYillar.map(() => ", ?, ?, ?").join("")})
`);
const exists = db.prepare("SELECT 1 FROM programs WHERE id = ?");
// Yeni satırda yazım/sözlük tutarlılığı — TAHMİN YOK, yalnız DB'de zaten var
// olan yazımı yeniden kullanma:
// - unitur: API sözlüğü (DEVLET/VAKIF/KKTC/YURTDIŞI) DB sözlüğünden (VAKIF MYO,
// YURTDISI KAMU…) farklı. Aynı üniversitenin mevcut satırlarında TEK bir
// unitur varsa o kullanılır; yoksa API değeri olduğu gibi yazılır. Aksi
// hâlde aynı kurumda iki unitur oluşur ve uniturOnar haklı olarak durur.
// - fakulte: API 2026'da BÜYÜK HARF veriyor; aynı üniversitede büyük harfe
// çevrilince aynı olan mevcut yazım varsa o kullanılır.
const bazAd = (ad: string) => ad.replace(/\s*\([^)]*\)\s*$/, "").trim();
const uniturlar = new Map<string, Set<string>>();
const fakulteler = new Map<string, Map<string, string>>();
for (const s of db
.prepare("SELECT DISTINCT universite, unitur, fakulte FROM programs")
.all() as { universite: string; unitur: string | null; fakulte: string | null }[]) {
const baz = bazAd(s.universite);
if (s.unitur) {
for (const k of [s.universite, baz]) {
const set = uniturlar.get(k) ?? new Set<string>();
set.add(s.unitur);
uniturlar.set(k, set);
}
}
if (s.fakulte) {
const m = fakulteler.get(baz) ?? new Map<string, string>();
const anahtar = s.fakulte.toLocaleUpperCase("tr-TR");
// Aynı anahtara iki farklı yazım düşerse belirsiz → yeniden kullanma
m.set(anahtar, m.has(anahtar) && m.get(anahtar) !== s.fakulte ? "" : s.fakulte);
fakulteler.set(baz, m);
}
}
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];
}
return api;
};
const fakulteSec = (universite: string, api: string | null): string | null => {
if (!api) return null;
const mevcut = fakulteler.get(bazAd(universite))?.get(api.toLocaleUpperCase("tr-TR"));
return mevcut || api;
};
let updated = 0;
let inserted = 0;
let skipped = 0;
const atlananlar: string[] = [];
const first = await fetchPage(0);
const totalPages = first.totalPages;
console.log(
`Toplam ${first.totalElements} kayıt, ${totalPages} sayfa çekilecek…`
);
db.transaction(() => {
const azamiRowid = (
db.prepare("SELECT COALESCE(MAX(rowid), 0) AS m FROM programs").get() as { m: number }
).m;
const once = oncekiYilSaglamasi(db, yil, azamiRowid);
const processPage = db.transaction((records: ApiRecord[]) => {
for (const r of records) {
for (const [id, r] of benzersiz) {
const tur = TUR_MAP[r.puanTuru?.trim() ?? ""];
if (!r.kilavuzKodu || !tur) {
skipped++;
// Puan türü yalnız YENİ satır için gerekir; mevcut satırın türü DB'de var.
// Kaynak türü vermiyorsa yeni satır UYDURULMAZ, atlanır ve raporlanır.
if (!tur && !exists.get(id)) {
atlananlar.push(`${id} ${r.universiteAdi} / ${r.birimAdi} (puanTuru="${r.puanTuru}")`);
continue;
}
const id = String(r.kilavuzKodu);
const vals = [
intOrNull(r.basariSirasi),
realOrNull(r.minPuan),
intOrNull(r.kontenjan),
yerlesen(r),
];
const eski = eskiKod(r);
if (exists.get(id)) {
update.run(...vals, id);
update.run(...vals, yil, eski, id);
updated++;
} else {
const gecmis = gecmisAlanlari(r).filter((g) => yil - g.fark >= ILK_YIL);
// "ÜNİVERSİTESİ- CERRAHPAŞA" gibi tire etrafı boşluklu varyantlar
// duplikat kurum adı üretmesin
const universite = uniAdiNormalize(r.universiteAdi);
insert.run(
id,
r.birimAdi,
// "ÜNİVERSİTESİ- CERRAHPAŞA" gibi tire etrafı boşluklu varyantlar
// duplikat kurum adı üretmesin
uniAdiNormalize(r.universiteAdi),
r.universiteTuru ?? null,
universite,
uniturSec(universite, r.universiteTuru ?? null),
r.ilAdi ?? null,
r.fymkAdi ?? null,
fakulteSec(universite, r.fymkAdi ?? null),
tur,
r.ogrenimSuresi ?? null,
r.birimTuruId === 47 ? 1 : 0,
...vals
...vals,
yil,
eski,
...gecmis.flatMap((g) => [g.sira, g.puan, g.kontenjan])
);
inserted++;
}
}
});
processPage(first.content);
for (let p = 1; p < totalPages; p++) {
await sleep(DELAY_MS);
const page = await fetchPage(p);
processPage(page.content);
process.stdout.write(`\rsayfa ${p + 1}/${totalPages}`);
const sonra = oncekiYilSaglamasi(db, yil, azamiRowid);
if (once !== sonra) {
dur(
"Mevcut satırların önceki yıl kolonları değişti — bu betik onlara dokunmamalı. Geri alındı.\n" +
`önce : ${once}\nsonra: ${sonra}`
);
}
const gorulen = (
db.prepare("SELECT COUNT(*) AS n FROM programs WHERE son_kilavuz_yili = ?").get(yil) as { n: number }
).n;
const beklenen = toplam - kodsuz - atlananlar.length;
if (gorulen !== beklenen) {
dur(
`son_kilavuz_yili=${yil} satır sayısı ${gorulen}, beklenen ${beklenen} ` +
`(API ${toplam} − atlanan ${kodsuz + atlananlar.length}). Geri alındı.`
);
}
console.log();
const stats = db
.prepare(
`SELECT COUNT(*) AS toplam,
SUM(CASE WHEN sira2025 IS NOT NULL THEN 1 ELSE 0 END) AS sirali
FROM programs`
)
.get() as { toplam: number; sirali: number };
metaYaz(db, `refresh_${yil}_api_toplam`, toplam);
metaYaz(db, `refresh_${yil}_atlanan`, kodsuz + atlananlar.length);
metaYaz(db, `refresh_${yil}_tarih`, new Date().toISOString());
metaYaz(db, `refresh_${yil}_ham`, path.basename(hamDosya));
})();
console.log(
`Güncellenen: ${updated}, eklenen: ${inserted}, atlanan: ${skipped}`
);
console.log(
`DB toplam: ${stats.toplam} program, 2025 sıralaması olan: ${stats.sirali}`
`Güncellenen: ${updated}, eklenen: ${inserted}, atlanan: ${kodsuz + atlananlar.length}`
);
for (const a of atlananlar) console.log(` atlandı: ${a}`);
// API'nin universiteTuru döndürmediği kayıtlar devlet/vakıf filtrelerinden
// düşmesin: her tazelemeden sonra unitur onarımı koşulur (idempotent).
db.transaction(() => uniturOnar(db))();
// Sorgu yolunun ifade indeksi — ifade veri-yillari.ts'ten; eski yılınki DROP.
console.log(`Efektif indeks: ${efektifIndeksiKur(db)}`);
const stats = db
.prepare(
`SELECT COUNT(*) AS toplam,
SUM(son_kilavuz_yili = ${yil}) AS guncel,
COUNT(sira${yil}) AS sirali
FROM programs`
)
.get() as { toplam: number; guncel: number; sirali: number };
console.log(
`DB toplam: ${stats.toplam} program; ${yil} kılavuzunda: ${stats.guncel}; ` +
`${yil} sıralaması olan: ${stats.sirali}`
);
db.pragma("wal_checkpoint(TRUNCATE)");
db.close();
console.log(
`\nSıradaki adım: pnpm gecmis-doldur --yil ${yil} --db <db> --ham ${path.relative(process.cwd(), hamDosya)}`
);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
if (process.argv[1]?.endsWith("refresh.ts")) calistir(main);

View File

@@ -38,6 +38,7 @@ import {
} from "../src/lib/tadimlik-havuzu";
import { anahtarVar } from "../src/lib/ai/client";
import { yapilandirilmisUret } from "../src/lib/ai/cagri";
import { ILK_YIL, VERI_YIL_SAYISI } from "../src/lib/veri-yillari";
// Next dışında koşuyoruz; .env dosyalarını elle yükle (mevcut env kazanır).
function envYukle(dosya: string): void {
@@ -72,8 +73,8 @@ KURALLAR (kesin):
riskli" etiketleriyle konuş.
- Programın "dilim" alanındaki "garanti" değeri teknik anahtardır; adaya dönük
hiçbir metinde bu kelimeyi kullanma, o dilimi "güvenli" diye an.
- trendOzeti: son 5 yılın taban sıralaması hareketi, TEK cümle (ör. "2022'den
beri taban 45B'den 38B'ye indi, bölüm zorlaşıyor").
- trendOzeti: son ${VERI_YIL_SAYISI} yılın taban sıralaması hareketi, TEK cümle
(ör. "2022'den beri taban 45B'den 38B'ye indi, bölüm zorlaşıyor").
- Adayın sıralaması yaklaşıktır; kesin sıra telaffuz etme, banda göre konuş.`;
function mockAktif(): boolean {
@@ -87,27 +88,30 @@ function mockMetin(p: Program, temsiliSira: number, kategori: string | null) {
riskNotu:
"(Dev mock) Son taban sıralaması sırana yakın; küçük bir kayma sonucu değiştirebilir.",
trendOzeti:
"(Dev mock) Son 5 yılda taban sıralaması görece stabil seyretti.",
`(Dev mock) Son ${VERI_YIL_SAYISI} yılda taban sıralaması görece stabil seyretti.`,
};
}
/** rapor.ts programBilgisi ile aynı snapshot şekli. */
function programBilgisi(p: Program) {
const gecmis = [p.sira2021, p.sira2022, p.sira2023, p.sira2024, p.sira2025];
const gecmis = p.siraGecmisi;
const gecmisVar = gecmis.some((s) => s != null);
return {
isim: p.isim,
universite: p.universite,
il: p.il,
unitur: p.unitur,
efektifSira: p.sira2025 ?? p.sira2024,
siraGecmisi: gecmis.some((s) => s != null) ? gecmis : undefined,
efektifSira: p.efektifSira,
siraGecmisi: gecmisVar ? gecmis : undefined,
ilkYil: gecmisVar ? ILK_YIL : undefined,
};
}
/**
* Deterministik program seçimi: kovanın temsili sırası için "dengeli" dilimden
* (boşsa "garanti") kategoriye ve üniversite tipine uyan, 2025'te en çok
* yerleşen alan program. Tanınır/dolu programlar tadımlıkta daha ikna edici.
* (boşsa "garanti") kategoriye ve üniversite tipine uyan, son yerleştirme
* yılında en çok yerleşen alan program. Tanınır/dolu programlar tadımlıkta
* daha ikna edici.
*/
function programSec(
temsiliSira: number,
@@ -125,7 +129,7 @@ function programSec(
: programlar;
if (uygunlar.length === 0) continue;
const secilen = [...uygunlar].sort(
(a, b) => (b.yerlesen2025 ?? -1) - (a.yerlesen2025 ?? -1),
(a, b) => (b.yerlesenSon ?? -1) - (a.yerlesenSon ?? -1),
)[0];
return { program: secilen, dilim };
}

228
scripts/veri-kalite.ts Normal file
View File

@@ -0,0 +1,228 @@
/**
* yokatlas.db veri kalitesi denetimi — SALT OKUNUR. Yayın öncesi kapı:
* bir ZORUNLU kontrol başarısızsa çıkış kodu 1 (CI/runbook durur).
* UYARI'lar çıkış kodunu etkilemez; insan gözüyle bakılır.
*
* Kullanım:
* pnpm veri-kalite --yil 2026 --db /tmp/deneme.db [--onceki data/yokatlas.db] [--arsivli]
*
* --onceki <db> Güncelleme ÖNCESİ DB. Verilirse: iki DB'de de bulunan her
* programın önceki yıl kolonları (sira/puan/kontenjan/yerlesen)
* birebir aynı mı (dolu değer değişmiş/silinmiş olamaz).
* --arsivli programs içinde güncel kılavuzda olmayan satır kalmamış olmalı.
*/
import path from "node:path";
import { EFEKTIF_SIRA_SQL, ILK_YIL } from "../src/lib/veri-yillari";
import { EFEKTIF_INDEKS, efektifSorguPlani } from "./db-index";
import {
argOku,
bayrakVar,
calistir,
dbAc,
dbArgumani,
dur,
kolonAdlari,
metaOku,
tabloVar,
yilArgumani,
} from "./yokatlas-goc";
const TURLER = ["SAYISAL", "EŞİT AĞIRLIK", "SÖZEL", "DİL", "TYT"];
async function main() {
const yil = yilArgumani();
const dbYolu = dbArgumani();
const oncekiArg = argOku("onceki");
const arsivli = bayrakVar("arsivli");
const db = dbAc(dbYolu, { saltOkunur: true });
const onceki = yil - 1;
let basarisiz = 0;
let uyari = 0;
const zorunlu = (ad: string, tamam: boolean, detay: string) => {
console.log(`${tamam ? "GEÇTİ " : "KALDI "} ${ad} — ${detay}`);
if (!tamam) basarisiz++;
};
const bilgi = (ad: string, sorunlu: boolean, detay: string) => {
console.log(`${sorunlu ? "UYARI " : "bilgi "} ${ad} — ${detay}`);
if (sorunlu) uyari++;
};
const sayi = (sql: string, ...args: unknown[]) =>
(db.prepare(sql).get(...args) as { n: number }).n;
const ornek = (sql: string, ...args: unknown[]) =>
(db.prepare(sql).raw().all(...args) as unknown[][]).map((r) => r.join("/")).join(" ; ");
console.log(`DB: ${dbYolu}\nYıl: ${yil} Efektif sıra: ${EFEKTIF_SIRA_SQL}\n`);
// 1. Dosya bütünlüğü
const butunluk = (db.pragma("integrity_check") as { integrity_check: string }[])
.map((r) => r.integrity_check)
.join(",");
zorunlu("integrity_check", butunluk === "ok", butunluk);
// 2. Şema
const kolonlar = new Set(kolonAdlari(db, "programs"));
const gerekli = [`sira${yil}`, `puan${yil}`, `kontenjan${yil}`, `yerlesen${yil}`, "son_kilavuz_yili", "eski_kod"];
const eksikKolon = gerekli.filter((k) => !kolonlar.has(k));
zorunlu("şema", eksikKolon.length === 0, eksikKolon.length ? `eksik: ${eksikKolon.join(", ")}` : "yıl kolonları + son_kilavuz_yili + eski_kod var");
if (eksikKolon.length) {
dur("Şema eksik; kalan kontroller koşulamaz.");
}
// 3. Kapsam: güncel satır = API toplamı − atlanan
const apiToplam = Number(metaOku(db, `refresh_${yil}_api_toplam`) ?? NaN);
const atlanan = Number(metaOku(db, `refresh_${yil}_atlanan`) ?? NaN);
const guncel = sayi("SELECT COUNT(*) AS n FROM programs WHERE son_kilavuz_yili = ?", yil);
zorunlu(
"kapsam",
Number.isFinite(apiToplam) && guncel === apiToplam - atlanan,
`son_kilavuz_yili=${yil}: ${guncel}; API toplamı ${apiToplam} − atlanan ${atlanan} = ${apiToplam - atlanan}`
);
// 4. Doluluk
const sirali = sayi(`SELECT COUNT(sira${yil}) AS n FROM programs WHERE son_kilavuz_yili = ?`, yil);
const oncekiSirali = sayi(`SELECT COUNT(sira${onceki}) AS n FROM programs WHERE son_kilavuz_yili = ?`, yil);
const doluOran = guncel ? sirali / guncel : 0;
zorunlu(
`sira${yil} doluluk`,
doluOran >= 0.7,
`${sirali}/${guncel} (%${(doluOran * 100).toFixed(1)}; eşik %70). Aynı satırlarda sira${onceki} dolu: ${oncekiSirali}`
);
const puanli = sayi(`SELECT COUNT(puan${yil}) AS n FROM programs WHERE son_kilavuz_yili = ?`, yil);
const kontenjanli = sayi(`SELECT COUNT(kontenjan${yil}) AS n FROM programs WHERE son_kilavuz_yili = ?`, yil);
bilgi(`${yil} diğer ölçüler`, kontenjanli < guncel * 0.95, `puan dolu ${puanli}, kontenjan dolu ${kontenjanli}, toplam ${guncel}`);
// 5. Yıllar arası tutarlılık: sira Y / sira Y-1 ∈ [0,5; 2]
const ikisiDolu = sayi(`SELECT COUNT(*) AS n FROM programs WHERE sira${yil} IS NOT NULL AND sira${onceki} IS NOT NULL`);
const aralikta = sayi(
`SELECT COUNT(*) AS n FROM programs WHERE sira${yil} IS NOT NULL AND sira${onceki} IS NOT NULL
AND sira${yil} * 1.0 / sira${onceki} BETWEEN 0.5 AND 2`
);
const aralikOran = ikisiDolu ? aralikta / ikisiDolu : 0;
zorunlu(
`sira${yil}/sira${onceki} ∈ [0,5; 2]`,
ikisiDolu > 0 && aralikOran >= 0.9,
`${aralikta}/${ikisiDolu} (%${(aralikOran * 100).toFixed(1)}; eşik %90)`
);
const ayniSira = sayi(
`SELECT COUNT(*) AS n FROM programs WHERE sira${yil} IS NOT NULL AND sira${yil} = sira${onceki} AND puan${yil} = puan${onceki}`
);
zorunlu(
"yıl kayması (kopya yıl)",
ikisiDolu > 0 && ayniSira / ikisiDolu < 0.02,
`sıra VE puanı ${onceki} ile birebir aynı satır: ${ayniSira}/${ikisiDolu} (çoksa ${yil} kolonuna ${onceki} verisi yazılmıştır)`
);
// 6. Değer aralıkları (tüm yıllar)
const yillar: number[] = [];
for (let y = ILK_YIL; y <= yil; y++) if (kolonlar.has(`sira${y}`)) yillar.push(y);
const bozukKosul = yillar
.map((y) => `sira${y} <= 0 OR sira${y} > 5000000 OR puan${y} <= 0 OR puan${y} > 600 OR kontenjan${y} < 0 OR yerlesen${y} < 0`)
.join(" OR ");
const bozuk = sayi(`SELECT COUNT(*) AS n FROM programs WHERE ${bozukKosul}`);
zorunlu("değer aralığı", bozuk === 0, `0/negatif/aşırı sıra-puan-kontenjan: ${bozuk}${bozuk ? ` ör. ${ornek(`SELECT id FROM programs WHERE ${bozukKosul} LIMIT 3`)}` : ""}`);
const siraPuanTutarsiz = sayi(
`SELECT COUNT(*) AS n FROM programs WHERE sira${yil} IS NOT NULL AND puan${yil} IS NULL`
);
zorunlu(`sira${yil} var, puan${yil} yok`, siraPuanTutarsiz === 0, `${siraPuanTutarsiz} satır`);
// 7. Kategorik alanlar
const bozukTur = sayi(
`SELECT COUNT(*) AS n FROM programs WHERE tur NOT IN (${TURLER.map(() => "?").join(",")}) OR onlisans NOT IN (0, 1)`,
...TURLER
);
zorunlu("tur/onlisans", bozukTur === 0, `geçersiz değer: ${bozukTur}`);
const tytTutarsiz = sayi("SELECT COUNT(*) AS n FROM programs WHERE (tur = 'TYT') <> (onlisans = 1) AND son_kilavuz_yili = ?", yil);
bilgi("TYT ↔ önlisans", tytTutarsiz > 0, `tur=TYT ile onlisans=1 örtüşmeyen güncel satır: ${tytTutarsiz}${tytTutarsiz ? ` ör. ${ornek("SELECT id, tur, onlisans FROM programs WHERE (tur = 'TYT') <> (onlisans = 1) AND son_kilavuz_yili = ? LIMIT 3", yil)}` : ""}`);
const uniturBos = sayi("SELECT COUNT(*) AS n FROM programs WHERE unitur IS NULL AND son_kilavuz_yili = ?", yil);
bilgi("unitur boş", uniturBos > 0, `güncel satırlarda ${uniturBos} (devlet/vakıf filtresinden düşer)${uniturBos ? ` ör. ${ornek("SELECT DISTINCT universite FROM programs WHERE unitur IS NULL AND son_kilavuz_yili = ? LIMIT 3", yil)}` : ""}`);
const uniturlar = ornek("SELECT COALESCE(unitur, '(NULL)'), COUNT(*) FROM programs GROUP BY unitur ORDER BY 2 DESC");
bilgi("unitur dağılımı", false, uniturlar);
const ilBos = sayi("SELECT COUNT(*) AS n FROM programs WHERE (il IS NULL OR TRIM(il) = '') AND son_kilavuz_yili = ?", yil);
bilgi("il boş", ilBos > 0, `güncel satırlarda ${ilBos}${ilBos ? ` ör. ${ornek("SELECT id, universite FROM programs WHERE (il IS NULL OR TRIM(il) = '') AND son_kilavuz_yili = ? LIMIT 3", yil)}` : ""}`);
// 8. Kontenjan–yerleşen
const tasan = sayi(`SELECT COUNT(*) AS n FROM programs WHERE yerlesen${yil} > kontenjan${yil} * 1.5 + 5`);
bilgi(`yerlesen${yil} ≫ kontenjan${yil}`, tasan > guncel * 0.02, `yerleşen > 1,5×genel kontenjan + 5: ${tasan} (yerleşen okul birincisi/şehit-gazi/deprem kontenjanlarını da içerir)${tasan ? ` ör. ${ornek(`SELECT id, kontenjan${yil}, yerlesen${yil} FROM programs WHERE yerlesen${yil} > kontenjan${yil} * 1.5 + 5 LIMIT 3`)}` : ""}`);
const doluAmaSirasiz = sayi(`SELECT COUNT(*) AS n FROM programs WHERE son_kilavuz_yili = ? AND sira${yil} IS NULL AND yerlesen${yil} > 0`, yil);
bilgi(`yerleşen var, sira${yil} yok`, false, `${doluAmaSirasiz} (API az yerleşenli programda sıra vermez; efektif sıra ${onceki}'e düşer)`);
// 9. Eski/çift satırlar
const eskiSatir = sayi("SELECT COUNT(*) AS n FROM programs WHERE son_kilavuz_yili IS NOT ?", yil);
if (arsivli) zorunlu("arşiv", eskiSatir === 0, `programs içinde güncel kılavuzda olmayan satır: ${eskiSatir}`);
else bilgi("arşivlenmemiş eski satır", eskiSatir > 0, `${eskiSatir} satır güncel kılavuzda yok ama sitede listelenir (efektif sıra ${onceki}'e düşer)`);
const ciftAd = sayi(
`SELECT COUNT(*) AS n FROM (SELECT 1 FROM programs WHERE son_kilavuz_yili = ?
GROUP BY universite, fakulte, isim, tur HAVING COUNT(*) > 1)`,
yil
);
bilgi("aynı adlı güncel program", false, `(universite, fakulte, isim, tur) yinelenen grup: ${ciftAd} (özel kontenjanlı adaş programlar olağandır)${ciftAd ? ` ör. ${ornek("SELECT universite, isim, COUNT(*) FROM programs WHERE son_kilavuz_yili = ? GROUP BY universite, fakulte, isim, tur HAVING COUNT(*) > 1 LIMIT 3", yil)}` : ""}`);
const eskiKodCakisma = sayi(
"SELECT COUNT(*) AS n FROM (SELECT eski_kod FROM programs WHERE eski_kod IS NOT NULL AND son_kilavuz_yili = ? GROUP BY eski_kod HAVING COUNT(*) > 1)",
yil
);
bilgi("eski_kod paylaşımı (bölünme)", eskiKodCakisma > 0, `birden çok güncel programın işaret ettiği eski kod: ${eskiKodCakisma}`);
// 10. netler
if (tabloVar(db, "netler")) {
const arsivVar = tabloVar(db, "programs_arsiv");
const yetim = sayi(
`SELECT COUNT(*) AS n FROM netler WHERE program_id NOT IN (SELECT id FROM programs)
${arsivVar ? "AND program_id NOT IN (SELECT id FROM programs_arsiv)" : ""}`
);
bilgi("yetim netler", yetim > 0, `ne programs ne arşivde karşılığı olan: ${yetim}`);
const netliGuncel = sayi(
"SELECT COUNT(DISTINCT n.program_id) AS n FROM netler n JOIN programs p ON p.id = n.program_id WHERE p.son_kilavuz_yili = ?",
yil
);
bilgi("netler kapsamı", false, `net satırı olan güncel program: ${netliGuncel}/${guncel}; yıl dağılımı: ${ornek("SELECT yil, COUNT(*) FROM netler GROUP BY yil ORDER BY yil")}`);
}
// 11. Sorgu planı
const plan = efektifSorguPlani(db);
zorunlu("sorgu planı", plan.some((s) => s.includes(EFEKTIF_INDEKS)), plan.join(" | "));
// 12. Önceki DB ile geçmiş yıl karşılaştırması
if (oncekiArg) {
const oncekiYol = path.resolve(process.cwd(), oncekiArg);
db.prepare("ATTACH DATABASE ? AS eski").run(oncekiYol);
const eskiKolonlar = new Set(
(db.prepare("PRAGMA eski.table_info(programs)").all() as { name: string }[]).map((c) => c.name)
);
const ortak: string[] = [];
for (let y = ILK_YIL; y < yil; y++) {
for (const o of ["sira", "puan", "kontenjan", "yerlesen"]) {
if (kolonlar.has(`${o}${y}`) && eskiKolonlar.has(`${o}${y}`)) ortak.push(`${o}${y}`);
}
}
const simdi = tabloVar(db, "programs_arsiv")
? `(SELECT id, ${ortak.join(", ")} FROM main.programs UNION ALL SELECT id, ${ortak.join(", ")} FROM main.programs_arsiv)`
: "main.programs";
const kaybolan = sayi(`SELECT COUNT(*) AS n FROM eski.programs e WHERE e.id NOT IN (SELECT id FROM ${simdi})`);
zorunlu("önceki DB: satır kaybı", kaybolan === 0, `önceki DB'de olup şimdi ne programs ne arşivde olan: ${kaybolan}`);
// Dolu değer değişmiş ya da NULL'a dönmüş olamaz; NULL → dolu (gecmis-doldur) serbest.
const degisen = sayi(
`SELECT COUNT(*) AS n FROM eski.programs e JOIN ${simdi} s ON s.id = e.id
WHERE ${ortak.map((k) => `(e.${k} IS NOT NULL AND e.${k} IS NOT s.${k})`).join(" OR ")}`
);
zorunlu("önceki DB: geçmiş yıllar korunmuş", degisen === 0, `${ortak.length} kolonda dolu değeri değişen/silinen satır: ${degisen}`);
const toplamlar = (tablo: string) =>
(db
.prepare(
`SELECT COUNT(*), TOTAL(sira${onceki}), TOTAL(puan${onceki}), TOTAL(kontenjan${onceki}), TOTAL(yerlesen${onceki}) FROM ${tablo}`
)
.raw()
.get() as number[]).join(" | ");
console.log(` ${onceki} sağlaması (count | Σsira | Σpuan | Σkontenjan | Σyerlesen)`);
console.log(` önceki DB : ${toplamlar("eski.programs")}`);
console.log(` şimdi, önceki DB'deki aynı id'ler : ${toplamlar(`${simdi} WHERE id IN (SELECT id FROM eski.programs)`)}`);
console.log(` şimdi, tüm satırlar (yeni dahil) : ${toplamlar(simdi)}`);
}
db.close();
console.log(`\nSonuç: ${basarisiz} ZORUNLU kontrol kaldı, ${uyari} uyarı.`);
if (basarisiz > 0) process.exit(1);
}
if (process.argv[1]?.endsWith("veri-kalite.ts")) calistir(main);

267
scripts/yokatlas-goc.ts Normal file
View File

@@ -0,0 +1,267 @@
/**
* yokatlas.db şema göçü — idempotent. Yeni yerleştirme yılı için kolonları,
* `son_kilavuz_yili` / `eski_kod` kolonlarını, `programs_arsiv` ve `veri_meta`
* tablolarını garanti eder. Hiçbir mevcut değeri silmez/ezmez.
*
* Betiklerin ortak yardımcıları (argüman okuma, DB açma) da buradadır;
* refresh / gecmis-doldur / eski-arsivle / detay / veri-kalite buradan alır.
*
* Kullanım: pnpm yokatlas:goc --yil 2026 --db data/yokatlas.db
*/
import fs from "node:fs";
import path from "node:path";
import Database from "better-sqlite3";
import { ILK_YIL, SON_YIL } from "../src/lib/veri-yillari";
export type Db = Database.Database;
/** Her yıl için tutulan dört ölçü ve SQLite tipi. */
export const YIL_OLCULERI = [
["sira", "INTEGER"],
["puan", "REAL"],
["kontenjan", "INTEGER"],
["yerlesen", "INTEGER"],
] as const;
// ── Komut satırı ─────────────────────────────────────────────────────────
/** `--ad deger` ya da `--ad=deger`. Yoksa undefined. */
export function argOku(ad: string): string | undefined {
const argv = process.argv.slice(2);
for (let i = 0; i < argv.length; i++) {
if (argv[i] === `--${ad}`) {
const sonraki = argv[i + 1];
return sonraki && !sonraki.startsWith("--") ? sonraki : undefined;
}
if (argv[i].startsWith(`--${ad}=`)) return argv[i].slice(ad.length + 3);
}
return undefined;
}
export function bayrakVar(ad: string): boolean {
return process.argv.slice(2).includes(`--${ad}`);
}
export class DurHatasi extends Error {}
/** Güvenlik kilidi: mesajı yazıp betiği durdurmak için fırlatılır. */
export function dur(mesaj: string): never {
throw new DurHatasi(mesaj);
}
/** main() sarmalayıcı: DurHatasi → temiz mesaj + çıkış kodu 1. */
export function calistir(main: () => Promise<void> | void): void {
Promise.resolve()
.then(main)
.catch((err) => {
if (err instanceof DurHatasi) console.error(`\nDURDU: ${err.message}`);
else console.error(err);
process.exit(1);
});
}
/**
* `--yil` ZORUNLU ve `SON_YIL` ile aynı olmalı. Varsayılan yok: betik hangi
* yılı yazdığını API'den ya da takvimden TAHMİN ETMEZ.
*/
export function yilArgumani(): number {
const ham = argOku("yil");
if (!ham) {
dur(
`--yil zorunlu (ör. --yil ${SON_YIL}). Varsayılan yok; yıl API kaydındaki ` +
"`yil` alanından da türetilmez."
);
}
const yil = Number.parseInt(ham, 10);
if (!Number.isInteger(yil) || String(yil) !== ham.trim()) {
dur(`--yil sayı olmalı: "${ham}"`);
}
if (yil !== SON_YIL) {
dur(
`--yil ${yil} ≠ SON_YIL ${SON_YIL} (src/lib/veri-yillari.ts). Yeni yıl ` +
"yükleniyorsa önce SON_YIL'i güncelle; betikler ve site aynı sabiti okur."
);
}
return yil;
}
/** `--db` ZORUNLU; dosya var olmalı. Yanlışlıkla boş DB yaratılmaz. */
export function dbArgumani(): string {
const ham = argOku("db");
if (!ham) {
dur(
"--db zorunlu (ör. --db data/yokatlas.db). Önce kopya üzerinde dene: " +
"cp data/yokatlas.db /tmp/deneme.db"
);
}
const yol = path.resolve(process.cwd(), ham);
if (!fs.existsSync(yol)) dur(`DB bulunamadı: ${yol}`);
return yol;
}
export function dbAc(yol: string, secenek: { saltOkunur?: boolean } = {}): Db {
const db = new Database(yol, {
readonly: secenek.saltOkunur ?? false,
fileMustExist: true,
});
if (!secenek.saltOkunur) db.pragma("journal_mode = WAL");
return db;
}
// ── Şema ─────────────────────────────────────────────────────────────────
export function tabloVar(db: Db, tablo: string): boolean {
return (
db
.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?")
.get(tablo) !== undefined
);
}
export function kolonAdlari(db: Db, tablo: string): string[] {
return (
db.prepare(`PRAGMA table_info(${tablo})`).all() as { name: string }[]
).map((c) => c.name);
}
function yilDogrula(yil: number): void {
if (!Number.isInteger(yil) || yil < ILK_YIL || yil > 2100) {
dur(`Geçersiz yıl: ${yil}`);
}
}
/**
* sira/puan/kontenjan/yerlesen<yil> kolonlarını (yoksa) ekler. İdempotent.
* Kontenjan kırılımı (gk/obk/…) ve kontenjanMeb kolonları BİLEREK eklenmez:
* `src/` içinde okuyan kod yok (KARARLAR 2026-09-21).
*/
export function yilKolonlariniGarantiEt(
db: Db,
yil: number,
tablo = "programs"
): string[] {
yilDogrula(yil);
const mevcut = new Set(kolonAdlari(db, tablo));
const eklenen: string[] = [];
for (const [olcu, tip] of YIL_OLCULERI) {
const kolon = `${olcu}${yil}`;
if (mevcut.has(kolon)) continue;
db.exec(`ALTER TABLE ${tablo} ADD COLUMN ${kolon} ${tip}`);
eklenen.push(kolon);
}
return eklenen;
}
/**
* `son_kilavuz_yili`: programın görüldüğü son tercih kılavuzu yılı.
* `eski_kod`: kılavuz kodu değiştiyse bir önceki kod (API `eskiKilavuzKodu`
* ya da gecmis-doldur'un benzersiz ad eşleşmesi).
*
* Geri doldurma TEK SEFERLİK — yalnız kolon ilk kez eklenirken koşar:
* kontenjan2025 dolu → 2025 kılavuzunda vardı. Gerisi NULL (bilinmiyor) kalır.
*/
export function kilavuzKolonlariniGarantiEt(db: Db): string[] {
const mevcut = new Set(kolonAdlari(db, "programs"));
const eklenen: string[] = [];
if (!mevcut.has("son_kilavuz_yili")) {
db.exec("ALTER TABLE programs ADD COLUMN son_kilavuz_yili INTEGER");
eklenen.push("son_kilavuz_yili");
if (mevcut.has("kontenjan2025")) {
const r = db
.prepare(
"UPDATE programs SET son_kilavuz_yili = 2025 WHERE kontenjan2025 IS NOT NULL"
)
.run();
console.log(` son_kilavuz_yili geri doldurma (2025): ${r.changes} satır`);
}
}
if (!mevcut.has("eski_kod")) {
db.exec("ALTER TABLE programs ADD COLUMN eski_kod TEXT");
eklenen.push("eski_kod");
}
return eklenen;
}
/**
* `programs_arsiv`: programs ile AYNI kolonlar (ad + tip + sıra). Yoksa kurar;
* varsa programs'a sonradan eklenmiş kolonları ona da ekler. Böylece
* `INSERT INTO programs_arsiv SELECT * FROM programs` her zaman geçerlidir…
* yine de taşıma kolon adlarıyla yapılır (bkz. eski-arsivle.ts).
*/
export function arsivTablosunuGarantiEt(db: Db): string[] {
const kolonlar = db.prepare("PRAGMA table_info(programs)").all() as {
name: string;
type: string;
}[];
if (!tabloVar(db, "programs_arsiv")) {
const tanim = kolonlar
.map((c) =>
c.name === "id"
? "id TEXT PRIMARY KEY"
: `${c.name} ${c.type || "TEXT"}`
)
.join(",\n ");
db.exec(`CREATE TABLE programs_arsiv (\n ${tanim}\n)`);
return ["programs_arsiv"];
}
const arsivde = new Set(kolonAdlari(db, "programs_arsiv"));
const eklenen: string[] = [];
for (const c of kolonlar) {
if (arsivde.has(c.name)) continue;
db.exec(
`ALTER TABLE programs_arsiv ADD COLUMN ${c.name} ${c.type || "TEXT"}`
);
eklenen.push(`programs_arsiv.${c.name}`);
}
return eklenen;
}
/** Koşu kayıtları (API toplamı, atlanan, ham dosya…): veri-kalite okur. */
export function metaTablosunuGarantiEt(db: Db): void {
db.exec(
"CREATE TABLE IF NOT EXISTS veri_meta (anahtar TEXT PRIMARY KEY, deger TEXT NOT NULL)"
);
}
export function metaYaz(db: Db, anahtar: string, deger: string | number): void {
metaTablosunuGarantiEt(db);
db.prepare(
"INSERT INTO veri_meta (anahtar, deger) VALUES (?, ?) " +
"ON CONFLICT(anahtar) DO UPDATE SET deger = excluded.deger"
).run(anahtar, String(deger));
}
export function metaOku(db: Db, anahtar: string): string | undefined {
if (!tabloVar(db, "veri_meta")) return undefined;
const r = db
.prepare("SELECT deger FROM veri_meta WHERE anahtar = ?")
.get(anahtar) as { deger: string } | undefined;
return r?.deger;
}
/** Tüm göç: yıl kolonları + kılavuz kolonları + arşiv + meta. İdempotent. */
export function yokatlasGoc(db: Db, yil: number): string[] {
const eklenen: string[] = [];
db.transaction(() => {
eklenen.push(...yilKolonlariniGarantiEt(db, yil));
eklenen.push(...kilavuzKolonlariniGarantiEt(db));
eklenen.push(...arsivTablosunuGarantiEt(db));
metaTablosunuGarantiEt(db);
})();
return eklenen;
}
// Doğrudan koşulduğunda
if (process.argv[1]?.endsWith("yokatlas-goc.ts")) {
calistir(() => {
const yil = yilArgumani();
const db = dbAc(dbArgumani());
const eklenen = yokatlasGoc(db, yil);
console.log(
eklenen.length
? `Göç tamam. Eklenen: ${eklenen.join(", ")}`
: "Göç tamam. Şema zaten güncel (değişiklik yok)."
);
db.close();
});
}

View File

@@ -4,6 +4,7 @@ import {
OG_CONTENT_TYPE,
OG_SIZE,
} from "@/lib/og-sayfa";
import { SON_YIL, TERCIH_YILI } from "@/lib/veri-yillari";
export const alt = "Bölüm taban puanları ve sıralamaları — KolayTercih";
export const size = OG_SIZE;
@@ -26,6 +27,6 @@ export default async function Image({
return createOgSayfaImage({
eyebrow: "Bölüm",
title: `${ad} taban puanları`,
description: `${sayi}2025 yerleştirme verileri · 2026 tercih rehberi`,
description: `${sayi}${SON_YIL} yerleştirme verileri · ${TERCIH_YILI} tercih rehberi`,
});
}

View File

@@ -3,9 +3,9 @@ import {
OG_CONTENT_TYPE,
OG_SIZE,
} from "@/lib/og-sayfa";
import { BASLIK_YILI, SON_YIL } from "@/lib/veri-yillari";
export const alt =
"Tüm bölümlerin taban puanları ve sıralamaları 2026 — KolayTercih";
export const alt = `Tüm bölümlerin taban puanları ve sıralamaları ${BASLIK_YILI} — KolayTercih`;
export const size = OG_SIZE;
export const contentType = OG_CONTENT_TYPE;
@@ -13,6 +13,6 @@ export default async function Image() {
return createOgSayfaImage({
eyebrow: "Katalog",
title: "Tüm bölümlerin taban puanları ve sıralamaları",
description: "Lisans ve önlisans programları · 2025 yerleştirme verileri",
description: `Lisans ve önlisans programları · ${SON_YIL} yerleştirme verileri`,
});
}

View File

@@ -1,16 +1,19 @@
import { Suspense } from "react";
import type { Metadata } from "next";
import { JsonLd, breadcrumbJsonLd, sayfaMetadata } from "@/lib/seo";
import { BASLIK_YILI, SON_YIL, TERCIH_YILI } from "@/lib/veri-yillari";
import { SiteFooter } from "@/components/site-footer";
import {
BolumKatalogu,
BolumKataloguSkeleton,
} from "@/features/katalog/components/bolum-listesi";
// Başlıktaki yıl tek kaynaktan (veri-yillari.ts); metadata ve h1 aynı metni okur.
const BASLIK = `Tüm Bölümlerin Taban Puanları ve Sıralamaları ${BASLIK_YILI}`;
export const metadata: Metadata = sayfaMetadata({
title: "Tüm Bölümlerin Taban Puanları ve Sıralamaları 2026",
description:
"Üniversite bölümlerinin 2025 yerleştirme taban puanları ve başarı sıralamaları: lisans ve önlisans programları kategori kategori, 2026 tercihleri için.",
title: BASLIK,
description: `Üniversite bölümlerinin ${SON_YIL} yerleştirme taban puanları ve başarı sıralamaları: lisans ve önlisans programları kategori kategori, ${TERCIH_YILI} tercihleri için.`,
path: "/bolumler",
});
@@ -26,7 +29,7 @@ export default function BolumlerPage() {
/>
<h1 className="font-heading text-3xl font-bold text-slate-900 sm:text-4xl">
Tüm Bölümlerin Taban Puanları ve Sıralamaları 2026
{BASLIK}
</h1>
<Suspense fallback={<BolumKataloguSkeleton />}>

View File

@@ -8,6 +8,7 @@ import {
OG_CONTENT_TYPE,
OG_SIZE,
} from "@/lib/og-sayfa";
import { SON_YIL } from "@/lib/veri-yillari";
export const alt = "Üniversite taban puanları ve sıralamaları — KolayTercih";
export const size = OG_SIZE;
@@ -26,7 +27,7 @@ export default async function Image({
if (uni?.programSayisi != null) {
parcalar.push(`${uni.programSayisi.toLocaleString("tr-TR")} program`);
}
parcalar.push("2025 yerleştirme verileri");
parcalar.push(`${SON_YIL} yerleştirme verileri`);
// Logo dosyası varsa OG kartına data URI olarak gömülür; okuma hatası
// kartı düşürmesin diye sessizce logosuz karta düşülür.

View File

@@ -3,9 +3,9 @@ import {
OG_CONTENT_TYPE,
OG_SIZE,
} from "@/lib/og-sayfa";
import { BASLIK_YILI, SON_YIL } from "@/lib/veri-yillari";
export const alt =
"Üniversitelerin taban puanları ve sıralamaları 2026 — KolayTercih";
export const alt = `Üniversitelerin taban puanları ve sıralamaları ${BASLIK_YILI} — KolayTercih`;
export const size = OG_SIZE;
export const contentType = OG_CONTENT_TYPE;
@@ -13,6 +13,6 @@ export default async function Image() {
return createOgSayfaImage({
eyebrow: "Katalog",
title: "Üniversitelerin taban puanları ve sıralamaları",
description: "Devlet ve vakıf üniversiteleri · 2025 yerleştirme verileri",
description: `Devlet ve vakıf üniversiteleri · ${SON_YIL} yerleştirme verileri`,
});
}

View File

@@ -1,16 +1,19 @@
import { Suspense } from "react";
import type { Metadata } from "next";
import { JsonLd, breadcrumbJsonLd, sayfaMetadata } from "@/lib/seo";
import { BASLIK_YILI, SON_YIL, TERCIH_YILI } from "@/lib/veri-yillari";
import { SiteFooter } from "@/components/site-footer";
import {
UniversiteKatalogu,
UniversiteKataloguSkeleton,
} from "@/features/katalog/components/universite-listesi";
// Başlıktaki yıl tek kaynaktan (veri-yillari.ts); metadata ve h1 aynı metni okur.
const BASLIK = `Üniversitelerin Taban Puanları ve Sıralamaları ${BASLIK_YILI}`;
export const metadata: Metadata = sayfaMetadata({
title: "Üniversitelerin Taban Puanları ve Sıralamaları 2026",
description:
"Devlet ve vakıf üniversitelerinin 2025 yerleştirme taban puanları, başarı sıralamaları ve kontenjanları — üniversite üniversite, 2026 tercihleri için.",
title: BASLIK,
description: `Devlet ve vakıf üniversitelerinin ${SON_YIL} yerleştirme taban puanları, başarı sıralamaları ve kontenjanları — üniversite üniversite, ${TERCIH_YILI} tercihleri için.`,
path: "/universiteler",
});
@@ -26,7 +29,7 @@ export default function UniversitelerPage() {
/>
<h1 className="font-heading text-3xl font-bold text-slate-900 sm:text-4xl">
Üniversitelerin Taban Puanları ve Sıralamaları 2026
{BASLIK}
</h1>
<Suspense fallback={<UniversiteKataloguSkeleton />}>

View File

@@ -1,30 +1,37 @@
import { TrendingDown, TrendingUp } from "lucide-react";
import type { Program } from "@/types/yokatlas";
import { ILK_YIL } from "@/lib/veri-yillari";
/** Taban sıra sütunlarının yapısal alt kümesi: hem DB programı hem de
* rapordaki kaydedilmiş sıra geçmişi (bkz. raporSiraSerisi) bu tipe uyar. */
export type SiraSerisi = Pick<
Program,
"sira2021" | "sira2022" | "sira2023" | "sira2024" | "sira2025"
>;
/** Taban sıra geçmişinin yapısal alt kümesi: hem DB programı hem de rapordaki
* kaydedilmiş sıra geçmişi (bkz. raporSiraSerisi) bu tipe uyar. Dizi `ilkYil`'a
* (yoksa veri penceresinin ilk yılına) demirlidir: eleman yılı
* `ilkYil + indeks`. Uzunluk sabit DEĞİL — eski kayıtlı raporlar daha kısa
* dizi taşır; yardımcılar dizinin kendi son iki elemanıyla çalışır. */
export type SiraSerisi = {
siraGecmisi: readonly (number | null)[];
ilkYil?: number;
};
/** Rapordaki [2021..2025] dizisini sütunlu seriye çevirir. */
export function raporSiraSerisi(gecmis?: (number | null)[]): SiraSerisi {
const g = gecmis ?? [];
return {
sira2021: g[0] ?? null,
sira2022: g[1] ?? null,
sira2023: g[2] ?? null,
sira2024: g[3] ?? null,
sira2025: g[4] ?? null,
};
/** Rapordaki kayıtlı diziyi seriye çevirir. Dizi DOLDURULMAZ: 5 elemanlı eski
* kayıt 5 elemanlı kalır, "son iki yıl" kaydın kendi son iki yılıdır. */
export function raporSiraSerisi(
gecmis?: (number | null)[],
ilkYil?: number,
): SiraSerisi {
return { siraGecmisi: gecmis ?? [], ilkYil };
}
/** Serinin son iki yılı: [bir önceki, son]. */
function sonIkiSira(program: SiraSerisi): [number | null, number | null] {
const g = program.siraGecmisi;
return [g[g.length - 2] ?? null, g[g.length - 1] ?? null];
}
export function programEtkinSira(program: SiraSerisi): number | null {
return program.sira2025 ?? program.sira2024;
const [onceki, son] = sonIkiSira(program);
return son ?? onceki;
}
/** 2021–2025 taban sıralamasını sonuç listesindeki kompakt çizgiyle gösterir. */
/** Taban sıralaması geçmişini sonuç listesindeki kompakt çizgiyle gösterir. */
export function ProgramSiraGecmisi({ program }: { program: SiraSerisi }) {
// Satır başına render edilen bileşen: filter/map/min/max/map/map'in 6 ayrı
// geçişi tek döngüde toplanır (js-combine-iterations, js-min-max-loop)
@@ -32,14 +39,10 @@ export function ProgramSiraGecmisi({ program }: { program: SiraSerisi }) {
let min = Infinity;
let max = -Infinity;
const ozetParcalari: string[] = [];
for (const [yil, sira] of [
[2021, program.sira2021],
[2022, program.sira2022],
[2023, program.sira2023],
[2024, program.sira2024],
[2025, program.sira2025],
] as const) {
const ilkYil = program.ilkYil ?? ILK_YIL;
for (const [indeks, sira] of program.siraGecmisi.entries()) {
if (sira == null) continue;
const yil = ilkYil + indeks;
yillar.push([yil, sira]);
if (sira < min) min = sira;
if (sira > max) max = sira;
@@ -89,12 +92,13 @@ export function ProgramSiraGecmisi({ program }: { program: SiraSerisi }) {
/** Son iki yılın tabanı %10'dan fazla değiştiğinde yönü işaretler. */
export function ProgramTabanTrendi({ program }: { program: SiraSerisi }) {
if (program.sira2025 == null || program.sira2024 == null) return null;
const [onceki, son] = sonIkiSira(program);
if (son == null || onceki == null) return null;
const oran = (program.sira2024 - program.sira2025) / program.sira2024;
const oran = (onceki - son) / onceki;
if (Math.abs(oran) < 0.1) return null;
const gecis = `${program.sira2024.toLocaleString("tr-TR")} → ${program.sira2025.toLocaleString("tr-TR")}`;
const gecis = `${onceki.toLocaleString("tr-TR")} → ${son.toLocaleString("tr-TR")}`;
if (oran > 0) {
return (
<TrendingUp

View File

@@ -25,6 +25,12 @@ import { VeriKaynagi } from "./veri-kaynagi";
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
import { SiteFooter } from "@/components/site-footer";
import { Skeleton } from "@/components/ui/skeleton";
import {
BASLIK_YILI,
ONCEKI_YIL,
SON_YIL,
TERCIH_YILI,
} from "@/lib/veri-yillari";
const sayi = (n: number) => n.toLocaleString("tr-TR");
@@ -65,20 +71,20 @@ function bolumSayfaDilimi(bolum: BolumDetay, sayfa: number) {
let minSira: number | null = null;
let maxSira: number | null = null;
let verisiz = 0;
// Sırası 2025'te olmayıp 2024'ten gelen satırlar: aralık metni yılı dürüstçe
// söylesin (tablonun Sıralama sütunu bu satırlarda "—" gösterir).
let yalniz2024 = 0;
// Sırası son yılda olmayıp bir önceki yıldan gelen satırlar: aralık metni
// yılı dürüstçe söylesin (tablonun Sıralama sütunu bu satırlarda "—" gösterir).
let yalnizOnceki = 0;
for (const p of dilim) {
const efektif = p.sira2025 ?? p.sira2024;
const efektif = p.efektifSira;
if (efektif == null) {
verisiz += 1;
continue;
}
if (p.sira2025 == null) yalniz2024 += 1;
if (p.siraSon == null) yalnizOnceki += 1;
if (minSira == null || efektif < minSira) minSira = efektif;
if (maxSira == null || efektif > maxSira) maxSira = efektif;
}
return { tumProgramlar, dilim, minSira, maxSira, verisiz, yalniz2024 };
return { tumProgramlar, dilim, minSira, maxSira, verisiz, yalnizOnceki };
}
/** <title> için kısa sıra: 2.425.061 → "2,43 mn", 95.400 → "95 bin". */
@@ -99,13 +105,13 @@ function siraAraligiMetni({
minSira,
maxSira,
verisiz,
yalniz2024,
yalnizOnceki,
}: ReturnType<typeof bolumSayfaDilimi>): string | null {
if (dilim.length === 0) return null;
if (minSira == null || maxSira == null) {
return "taban verisi olmayan programlar";
}
const yil = siraYiliEtiketi(yalniz2024, dilim.length - verisiz);
const yil = siraYiliEtiketi(yalnizOnceki, dilim.length - verisiz);
const aralik =
minSira === maxSira
? `${yil} taban sırası ${sayi(minSira)} olan programlar`
@@ -134,8 +140,8 @@ export function bolumSayfaMetadata(slug: string, sayfa: number): Metadata {
const meta = sayfaMetadata({
title: baslik,
description: aralik
? `${bolum.ad} taban puanları, sayfa ${sayfa}: ${aralik}. Toplam ${stats.toplam} programın son yerleştirme verileri (2025; 2025'te verisi olmayanlarda 2024) — taban puanı, başarı sıralaması ve kontenjan.`
: `${bolum.ad} taban puanları ve başarı sıralamaları tablosunun ${sayfa}. sayfası — toplam ${stats.toplam} programın 2025 yerleştirme verileri.`,
? `${bolum.ad} taban puanları, sayfa ${sayfa}: ${aralik}. Toplam ${stats.toplam} programın son yerleştirme verileri (${SON_YIL}; o yıl verisi olmayanlarda ${ONCEKI_YIL}) — taban puanı, başarı sıralaması ve kontenjan.`
: `${bolum.ad} taban puanları ve başarı sıralamaları tablosunun ${sayfa}. sayfası — toplam ${stats.toplam} programın ${SON_YIL} yerleştirme verileri.`,
path: `/bolum/${slug}/sayfa/${sayfa}`,
openGraph: {
images: ustSegmentOgGorseli(
@@ -151,8 +157,8 @@ export function bolumSayfaMetadata(slug: string, sayfa: number): Metadata {
? ` En iyi taban sıralaması ${sayi(stats.minSira)}.`
: "";
return sayfaMetadata({
title: `${bolum.ad} Taban Puanları ve Başarı Sıralamaları 2026`,
description: `${bolum.ad} 2025 yerleştirme verileri: ${stats.toplam} programın taban puanı ve sıralaması, ${stats.devlet} devlet / ${stats.vakif} vakıf.${enIyi} 2026 tercih rehberi.`,
title: `${bolum.ad} Taban Puanları ve Başarı Sıralamaları ${BASLIK_YILI}`,
description: `${bolum.ad} ${SON_YIL} yerleştirme verileri: ${stats.toplam} programın taban puanı ve sıralaması, ${stats.devlet} devlet / ${stats.vakif} vakıf.${enIyi} ${TERCIH_YILI} tercih rehberi.`,
path: `/bolum/${slug}`,
});
}
@@ -230,7 +236,7 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) {
</h1>
{ilkSayfa ? (
<p className="mt-3 max-w-2xl text-sm leading-6 text-muted-foreground sm:text-base">
{sayi(tumProgramlar.length)} üniversite programının 2025 taban
{sayi(tumProgramlar.length)} üniversite programının {SON_YIL} taban
puanlarını, başarı sıralamalarını ve kontenjanlarını incele.
</p>
) : (

View File

@@ -4,6 +4,7 @@ import { KATEGORILER } from "@/lib/kategoriler";
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
import { PagePixelDivider } from "@/components/pixel-decor";
import { Skeleton } from "@/components/ui/skeleton";
import { SON_YIL, TERCIH_YILI } from "@/lib/veri-yillari";
const sayi = (n: number) => n.toLocaleString("tr-TR");
@@ -81,11 +82,11 @@ export function BolumKatalogu() {
return (
<>
<p className="mt-4 max-w-3xl text-sm leading-7 text-slate-600">
{sayi(hepsi.length)} bölümün 2025 YKS yerleştirme verileri: taban
{sayi(hepsi.length)} bölümün {SON_YIL} YKS yerleştirme verileri: taban
puanları, başarı sıralamaları, kontenjanlar ve devlet/vakıf dağılımı.
Bölüme tıklayınca o bölümü sunan tüm üniversitelerin tablosunu görürsün.
Veriler YÖK Atlas 2025 yerleştirme sonuçlarına dayanır; 2026 tercihleri
için rehber niteliğindedir.
Veriler YÖK Atlas {SON_YIL} yerleştirme sonuçlarına dayanır;{" "}
{TERCIH_YILI} tercihleri için rehber niteliğindedir.
</p>
<PagePixelDivider seed={29} className="mt-8" />

View File

@@ -5,6 +5,7 @@ import Link from "next/link";
import { ChevronDown, ChevronUp } from "lucide-react";
import type { Program } from "@/types/yokatlas";
import { trBaslikDuzeni } from "@/lib/slug";
import { VERI_YIL_SAYISI } from "@/lib/veri-yillari";
import { Sayfalama, SAYFA_BOYU } from "./sayfalama";
import {
ProgramSiraGecmisi,
@@ -24,14 +25,14 @@ import {
const sayi = (n: number) => n.toLocaleString("tr-TR");
type SiralamaAlani = "sira2025" | "puan2025" | "kontenjan2025";
type SiralamaAlani = "siraSon" | "puanSon" | "kontenjanSon";
type SiralamaYonu = "artan" | "azalan";
export type BolumProgrami = Program & { universiteSlug: string | null };
const ALAN_ETIKETLERI: Record<SiralamaAlani, string> = {
sira2025: "Sıralama",
puan2025: "Puan",
kontenjan2025: "Kontenjan",
siraSon: "Sıralama",
puanSon: "Puan",
kontenjanSon: "Kontenjan",
};
function SiralamaDugmesi({
@@ -157,11 +158,11 @@ export function BolumProgramListesi({
<TableRow>
<TableHead>Üniversite</TableHead>
<TableHead className="hidden text-right md:table-cell">
Son 5 yıl
Son {VERI_YIL_SAYISI} yıl
</TableHead>
<TableHead className="hidden text-right lg:table-cell">
<SiralamaDugmesi
alan="puan2025"
alan="puanSon"
aktifAlan={aktifAlan}
yon={yon}
onSirala={sirala}
@@ -169,7 +170,7 @@ export function BolumProgramListesi({
</TableHead>
<TableHead className="hidden text-right sm:table-cell">
<SiralamaDugmesi
alan="kontenjan2025"
alan="kontenjanSon"
aktifAlan={aktifAlan}
yon={yon}
onSirala={sirala}
@@ -177,7 +178,7 @@ export function BolumProgramListesi({
</TableHead>
<TableHead className="text-right">
<SiralamaDugmesi
alan="sira2025"
alan="siraSon"
aktifAlan={aktifAlan}
yon={yon}
onSirala={sirala}
@@ -243,17 +244,17 @@ export function BolumProgramListesi({
<ProgramSiraGecmisi program={p} />
</TableCell>
<TableCell className="hidden text-right text-xs tabular-nums text-slate-500 lg:table-cell">
{p.puan2025 != null
? p.puan2025.toFixed(2).replace(".", ",")
{p.puanSon != null
? p.puanSon.toFixed(2).replace(".", ",")
: "—"}
</TableCell>
<TableCell className="hidden text-right text-xs tabular-nums text-slate-500 sm:table-cell">
{p.kontenjan2025 != null ? `${sayi(p.kontenjan2025)} kişi` : "—"}
{p.kontenjanSon != null ? `${sayi(p.kontenjanSon)} kişi` : "—"}
</TableCell>
<TableCell className="text-right text-xs tabular-nums text-slate-500">
<span className="inline-flex items-center gap-1">
<ProgramTabanTrendi program={p} />
{p.sira2025 != null ? `~${sayi(p.sira2025)}.` : "—"}
{p.siraSon != null ? `~${sayi(p.siraSon)}.` : "—"}
</span>
</TableCell>
<TableCell className="pr-3 text-right">

View File

@@ -1,6 +1,7 @@
import type { ReactNode } from "react";
import Link from "next/link";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { ONCEKI_YIL, SON_YIL } from "@/lib/veri-yillari";
/** Programatik tablo sayfaları için satır dilimi boyutu. */
export const SAYFA_BOYU = 50;
@@ -13,10 +14,19 @@ export function sayfaDilimi<T>(liste: readonly T[], sayfa: number): T[] {
return liste.slice((sayfa - 1) * SAYFA_BOYU, sayfa * SAYFA_BOYU);
}
/** Aralıktaki sıraların hangi yıldan geldiği: hepsi 2025 / hepsi 2024 / karışık. */
export function siraYiliEtiketi(yalniz2024: number, siraliSayi: number): string {
if (yalniz2024 === 0) return "2025";
return yalniz2024 === siraliSayi ? "2024" : "2024–2025";
/**
* Aralıktaki sıraların hangi yıldan geldiği: hepsi son yıl / hepsi bir önceki
* yıl / karışık. `yalnizOnceki`: sırası son yılda olmayıp bir önceki yıldan
* gelen satır sayısı; `siraliSayi`: sırası olan toplam satır.
*/
export function siraYiliEtiketi(
yalnizOnceki: number,
siraliSayi: number,
): string {
if (yalnizOnceki === 0) return String(SON_YIL);
return yalnizOnceki === siraliSayi
? String(ONCEKI_YIL)
: `${ONCEKI_YIL}–${SON_YIL}`;
}
/**

View File

@@ -34,6 +34,7 @@ import { UniLogo } from "@/components/uni-logo";
import { uniLogoYolu } from "@/lib/uni-logolar";
import { Skeleton } from "@/components/ui/skeleton";
import type { Program } from "@/types/yokatlas";
import { BASLIK_YILI, SON_YIL, TERCIH_YILI } from "@/lib/veri-yillari";
const sayi = (n: number) => n.toLocaleString("tr-TR");
@@ -91,13 +92,13 @@ function fakulteOzeti({
if (adlar.length === 0) return null;
if (adlar.length === 1) {
const siralar = dilim
.map((p) => p.sira2025 ?? p.sira2024)
.map((p) => p.efektifSira)
.filter((n): n is number => n != null);
if (siralar.length === 0) return adlar[0];
const min = Math.min(...siralar);
const max = Math.max(...siralar);
const yil = siraYiliEtiketi(
dilim.filter((p) => p.sira2025 == null && p.sira2024 != null).length,
dilim.filter((p) => p.siraSon == null && p.efektifSira != null).length,
siralar.length,
);
return min === max
@@ -116,10 +117,10 @@ export function uniSayfaMetadata(slug: string, sayfa: number): Metadata {
// farklılaştırıcı (o sayfadaki birimler) açıklamaya ve gövdeye gider.
const birimler = fakulteOzeti(uniSayfaDilimi(uni, sayfa));
return sayfaMetadata({
title: `${uni.ad} Taban Puanları 2026 (Sayfa ${sayfa})`,
title: `${uni.ad} Taban Puanları ${BASLIK_YILI} (Sayfa ${sayfa})`,
description: birimler
? `${uni.ad} taban puanları, sayfa ${sayfa}: ${birimler}. Toplam ${uni.programSayisi} programın 2025 yerleştirme verileri — taban puanı, başarı sıralaması ve kontenjan.`
: `${uni.ad} taban puanları ve başarı sıralamaları tablosunun ${sayfa}. sayfası — toplam ${uni.programSayisi} programın 2025 yerleştirme verileri.`,
? `${uni.ad} taban puanları, sayfa ${sayfa}: ${birimler}. Toplam ${uni.programSayisi} programın ${SON_YIL} yerleştirme verileri — taban puanı, başarı sıralaması ve kontenjan.`
: `${uni.ad} taban puanları ve başarı sıralamaları tablosunun ${sayfa}. sayfası — toplam ${uni.programSayisi} programın ${SON_YIL} yerleştirme verileri.`,
path: `/universite/${slug}/sayfa/${sayfa}`,
openGraph: {
images: ustSegmentOgGorseli(
@@ -130,8 +131,8 @@ export function uniSayfaMetadata(slug: string, sayfa: number): Metadata {
});
}
return sayfaMetadata({
title: `${uni.ad} Taban Puanları ve Başarı Sıralamaları 2026`,
description: `${uni.ad} 2025 yerleştirme verileri: ${uni.programSayisi} programın taban puanları, başarı sıralamaları ve kontenjanları${uni.il ? ` (${trBaslikDuzeni(uni.il)})` : ""}. 2026 tercih rehberi.`,
title: `${uni.ad} Taban Puanları ve Başarı Sıralamaları ${BASLIK_YILI}`,
description: `${uni.ad} ${SON_YIL} yerleştirme verileri: ${uni.programSayisi} programın taban puanları, başarı sıralamaları ve kontenjanları${uni.il ? ` (${trBaslikDuzeni(uni.il)})` : ""}. ${TERCIH_YILI} tercih rehberi.`,
path: `/universite/${slug}`,
});
}
@@ -241,7 +242,7 @@ export function UniversiteIcerik({
<div className="mt-4 flex items-center gap-4">
<UniLogo slug={slug} ad={uni.ad} boy="lg" />
<h1 className="font-heading text-3xl font-bold text-slate-900 sm:text-4xl">
{uni.ad} Taban Puanları 2026
{uni.ad} Taban Puanları {BASLIK_YILI}
{ilkSayfa ? "" : ` — Sayfa ${sayfa}`}
</h1>
</div>
@@ -250,7 +251,7 @@ export function UniversiteIcerik({
<>
<p className="mt-4 max-w-3xl text-sm leading-7 text-slate-600">
{uni.ad}
{uni.il ? ` (${trBaslikDuzeni(uni.il)})` : ""}, 2025 YKS
{uni.il ? ` (${trBaslikDuzeni(uni.il)})` : ""}, {SON_YIL} YKS
yerleştirme sonuçlarına göre{" "}
<strong>{sayi(uni.programSayisi)} programda</strong> öğrenci aldı
{uni.fakulteSayisi > 1
@@ -267,10 +268,11 @@ export function UniversiteIcerik({
{uni.stats.toplamKontenjan > 0 ? (
<> Toplam kontenjan {sayi(uni.stats.toplamKontenjan)}.</>
) : null}{" "}
Aşağıdaki tablolar 2026 tercihleri için rehber niteliğindedir;{" "}
Aşağıdaki tablolar {TERCIH_YILI} tercihleri için rehber
niteliğindedir;{" "}
<em>
2026 taban puanları ve sıralamaları, yerleştirme sonuçları
açıklandığında netleşir.
{TERCIH_YILI} taban puanları ve sıralamaları, yerleştirme
sonuçları açıklandığında netleşir.
</em>
</p>

View File

@@ -5,6 +5,7 @@ import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
import { PagePixelDivider } from "@/components/pixel-decor";
import { UniLogo } from "@/components/uni-logo";
import { Skeleton } from "@/components/ui/skeleton";
import { SON_YIL, TERCIH_YILI } from "@/lib/veri-yillari";
const sayi = (n: number) => n.toLocaleString("tr-TR");
@@ -49,11 +50,11 @@ export function UniversiteKatalogu() {
return (
<>
<p className="mt-4 max-w-3xl text-sm leading-7 text-slate-600">
{sayi(hepsi.length)} üniversitenin 2025 YKS yerleştirme verileri.
{sayi(hepsi.length)} üniversitenin {SON_YIL} YKS yerleştirme verileri.
Üniversiteye tıklayınca tüm programlarının taban puanlarını, başarı
sıralamalarını ve kontenjanlarını fakülte fakülte görürsün. Veriler YÖK
Atlas 2025 yerleştirme sonuçlarına dayanır; 2026 tercihleri için rehber
niteliğindedir.
Atlas {SON_YIL} yerleştirme sonuçlarına dayanır; {TERCIH_YILI} tercihleri
için rehber niteliğindedir.
</p>
<PagePixelDivider seed={63} className="mt-8" />

View File

@@ -2,6 +2,7 @@
import Link from "next/link";
import type { Program } from "@/types/yokatlas";
import { SON_YIL, VERI_YIL_SAYISI } from "@/lib/veri-yillari";
import {
ProgramSiraGecmisi,
ProgramTabanTrendi,
@@ -48,10 +49,10 @@ export function UniversiteProgramTablosu({
<TableRow>
<TableHead>Program</TableHead>
<TableHead className="hidden text-right md:table-cell">
Son 5 yıl
Son {VERI_YIL_SAYISI} yıl
</TableHead>
<TableHead className="hidden text-right lg:table-cell">
2025 puan
{SON_YIL} puan
</TableHead>
<TableHead className="hidden text-right sm:table-cell">
Kontenjan
@@ -93,13 +94,13 @@ export function UniversiteProgramTablosu({
<ProgramSiraGecmisi program={p} />
</TableCell>
<TableCell className="hidden text-right text-xs tabular-nums text-slate-500 lg:table-cell">
{p.puan2025 != null
? p.puan2025.toFixed(2).replace(".", ",")
{p.puanSon != null
? p.puanSon.toFixed(2).replace(".", ",")
: "—"}
</TableCell>
<TableCell className="hidden text-right text-xs tabular-nums text-slate-500 sm:table-cell">
{p.kontenjan2025 != null
? `${sayi(p.kontenjan2025)} kişi`
{p.kontenjanSon != null
? `${sayi(p.kontenjanSon)} kişi`
: "—"}
</TableCell>
<TableCell className="text-right text-xs tabular-nums text-slate-500">

View File

@@ -1,7 +1,8 @@
import { KATALOG_VERI_TARIHI } from "@/lib/seo";
import { cn } from "@/lib/utils";
import { SON_YIL } from "@/lib/veri-yillari";
// Veri tarihinin görünür biçimi ("9 Ağustos 2026") — tek biçim kaynağı; ana
// Veri tarihinin görünür biçimi ("21 Eylül 2026") — tek biçim kaynağı; ana
// sayfa hero'sundaki veri satırı da bunu kullanır.
export const KATALOG_TARIH_METNI = new Date(
`${KATALOG_VERI_TARIHI}T00:00:00Z`,
@@ -24,7 +25,7 @@ export function VeriKaynagi({
return (
<p className={cn("mt-3 text-xs leading-5 text-slate-500", className)}>
{children}
Kaynak: YÖK Atlas 2025 yerleştirme sonuçları. Son güncelleme:{" "}
Kaynak: YÖK Atlas {SON_YIL} yerleştirme sonuçları. Son güncelleme:{" "}
<time dateTime={KATALOG_VERI_TARIHI}>{KATALOG_TARIH_METNI}</time>.
</p>
);

View File

@@ -24,8 +24,7 @@ type ProgramOzet = Pick<
| "il"
| "unitur"
| "tur"
| "sira2025"
| "sira2024"
| "efektifSira"
>;
const PROFILSIZ_EKLEME_TOAST_ID = "profilsiz-ekleme";

View File

@@ -22,6 +22,7 @@ import type {
} from "@/types/yokatlas";
import { DILIM_ETIKET, riskHesapla, type RiskSeviyesi } from "@/lib/risk";
import { bolumSayfaSlug, uniSayfaSlug } from "@/lib/slug";
import { VERI_YIL_SAYISI } from "@/lib/veri-yillari";
import {
ProgramSiraGecmisi,
ProgramTabanTrendi,
@@ -239,7 +240,7 @@ export function ProgramTablosu({
<TableHead className="w-8" />
<TableHead>Program</TableHead>
<TableHead className="hidden text-right md:table-cell">
Son 5 yıl
Son {VERI_YIL_SAYISI} yıl
</TableHead>
<TableHead className="hidden text-right sm:table-cell">
Kontenjan
@@ -357,19 +358,19 @@ function ProgramSatiri({
<TableCell
className="hidden text-right text-xs tabular-nums text-slate-500 sm:table-cell"
title={
p.kontenjan2025 != null && p.yerlesen2025 != null
? `${p.kontenjan2025} kontenjan, ${p.yerlesen2025} yerleşen`
p.kontenjanSon != null && p.yerlesenSon != null
? `${p.kontenjanSon} kontenjan, ${p.yerlesenSon} yerleşen`
: undefined
}
>
{p.kontenjan2025 != null ? (
{p.kontenjanSon != null ? (
<span className="inline-flex items-center gap-1.5">
{p.yerlesen2025 != null && p.yerlesen2025 < p.kontenjan2025 ? (
{p.yerlesenSon != null && p.yerlesenSon < p.kontenjanSon ? (
<span className="rounded bg-amber-50 px-1 py-px text-[10px] font-medium leading-4 text-amber-600">
boş kaldı
</span>
) : null}
{p.kontenjan2025.toLocaleString("tr-TR")} kişi
{p.kontenjanSon.toLocaleString("tr-TR")} kişi
</span>
) : (
"—"

View File

@@ -28,8 +28,7 @@ export type BekleyenProgram = {
| "il"
| "unitur"
| "tur"
| "sira2025"
| "sira2024"
| "efektifSira"
>;
kaynak: ManuelKaynak;
adaySira?: number;

View File

@@ -8,7 +8,8 @@ import type {
UniturGrubu,
} from "@/types/yokatlas";
// Tabloda kullanılmayan alanlar (fakulte, puan2025) client'a serileşmesin.
// Tabloda kullanılmayan alanlar (fakulte, puanSon, siraSon) client'a
// serileşmesin.
// Alanlar tek tek seçilir: Program'a alan eklenirse ProgramListeSatiri
// üzerinden derleyici burada da karar vermeye zorlar.
export function programOzetle(p: Program): ProgramListeSatiri {
@@ -20,13 +21,10 @@ export function programOzetle(p: Program): ProgramListeSatiri {
il: p.il,
tur: p.tur,
sure: p.sure,
sira2025: p.sira2025,
sira2024: p.sira2024,
sira2023: p.sira2023,
sira2022: p.sira2022,
sira2021: p.sira2021,
kontenjan2025: p.kontenjan2025,
yerlesen2025: p.yerlesen2025,
siraGecmisi: p.siraGecmisi,
efektifSira: p.efektifSira,
kontenjanSon: p.kontenjanSon,
yerlesenSon: p.yerlesenSon,
};
}

View File

@@ -158,10 +158,8 @@ export function sunucuylaBirlestir(sunucuItems: ManuelTercih[]): ManuelTercih[]
return liste;
}
export function efektifSiraAl(
p: Pick<Program, "sira2025" | "sira2024">,
): number | null {
return p.sira2025 ?? p.sira2024 ?? null;
export function efektifSiraAl(p: Pick<Program, "efektifSira">): number | null {
return p.efektifSira ?? null;
}
/** Program satırını manuel liste kaydına çevirir. */
@@ -174,8 +172,7 @@ export function programdanManuelTercih(
| "il"
| "unitur"
| "tur"
| "sira2025"
| "sira2024"
| "efektifSira"
>,
adaySira?: number | null,
kaynak: ManuelKaynak = "sonuc",

View File

@@ -16,7 +16,7 @@ flowchart TD
T("Sıralamanla açılan tüm programlar") --> A("1. adım · Alan seçimi<br/>alanlar bölümün adına ve bağlı olduğu fakülteye göre belirlenir, yalnızca ulaşabildiklerin listelenir")
A --> B("2. adım · İl seçimi<br/>programı olan tüm iller, seçtiğin alanlardaki program sayısına göre yeniden sayılır")
B --> C("3. adım · Devlet / Vakıf<br/>sayılar hem alanı hem ili hesaba katar")
C --> H("Aday havuzu<br/>elde kalan gerçek programlar")
C --> H("Aday havuzu<br/>elde kalan gerçek programlar — yalnızca en son tercih kılavuzunda yer alanlar<br/>kapanan ya da kodu değişip eski kaydı kalan programlar önerilmez")
H --> S{"Havuzda 24'ten az program mı kaldı?"}
S -- "evet" --> G("Filtreler adım adım gevşetilir<br/>önce il, sonra alan — listede açıkça belirtilir")
G --> H
@@ -44,10 +44,12 @@ export function HuniSemasi() {
return (
<ElCizimiSema
tanim={SEMA}
ariaLabel="Sihirbaz akış şeması: sıralamanla açılan programlar alan, il ve devlet/vakıf adımlarıyla aday havuzuna daraltılır; alanlar bölümün adına ve bağlı olduğu fakülteye göre belirlenir, il adımında programı olan tüm iller listelenir; havuz 24'ün altına düşerse filtreler gevşetilir, yine de dar kalırsa liste eldeki programlarla kurulur ve dağılım havuza uyarlanır; yapay zekâ havuzdan üstte hayal, ortada dengeli, sonda güvenli olacak şekilde en fazla 24 tercih seçip her satıra gerekçe yazar; risk notu ve trend özeti gerçek sıralama geçmişinden hesaplanır."
ariaLabel="Sihirbaz akış şeması: sıralamanla açılan programlar alan, il ve devlet/vakıf adımlarıyla aday havuzuna daraltılır; havuza yalnızca en son tercih kılavuzunda yer alan programlar girer, kapanan ya da kodu değişip eski kaydı kalan programlar önerilmez; alanlar bölümün adına ve bağlı olduğu fakülteye göre belirlenir, il adımında programı olan tüm iller listelenir; havuz 24'ün altına düşerse filtreler gevşetilir, yine de dar kalırsa liste eldeki programlarla kurulur ve dağılım havuza uyarlanır; yapay zekâ havuzdan üstte hayal, ortada dengeli, sonda güvenli olacak şekilde en fazla 24 tercih seçip her satıra gerekçe yazar; risk notu ve trend özeti gerçek sıralama geçmişinden hesaplanır."
figcaption={
<>
Her adım havuzu biraz daha daraltır; yapay zekâ listeyi bu havuzun{" "}
Havuza yalnızca en son tercih kılavuzunda yer alan programlar girer;
kapanan ya da kodu değişip eski kaydı kalan programlar önerilmez. Her
adım havuzu biraz daha daraltır; yapay zekâ listeyi bu havuzun{" "}
<span className="font-medium text-slate-700">
dışına asla çıkmadan
</span>{" "}

View File

@@ -47,8 +47,8 @@ export function MeraklisinaIcerik() {
öğrencinin Türkiye sıralamasıdır. Senin sıralaman bu sayıdan
küçükse (yani daha iyiysen), geçen yılın koşullarında o bölüme
yerleşebilirdin demektir. Sistemde her programın en güncel taban
sıralaması kayıtlıdır; bir bölümün 2025 verisi henüz oluşmamışsa
bir önceki yılınki kullanılır.
sıralaması kayıtlıdır; bir bölüm için en son yılın verisi henüz
oluşmamışsa bir önceki yılınki kullanılır.
</p>
</section>
@@ -116,10 +116,13 @@ export function MeraklisinaIcerik() {
tabloya da uygulanır. Adımlar bitince elde kalan gerçek
programlar bir <strong>aday havuzu</strong>{" "}
olur ve yapay zekâya öyle gider: ona boş bir sayfa değil, bu havuz
verilir. Aşağıdaki şema akışın tamamını gösterir — havuz
24&apos;ün altına düşerse filtrelerin nasıl gevşetildiği, yine de
yetmezse listenin eldeki programlarla, sana açıkça belirtilerek
nasıl kurulduğu da dahil.
verilir. Havuza yalnızca{" "}
<strong>en son tercih kılavuzunda yer alan</strong>{" "}
programlar girer: kapanmış ya da kodu değiştiği için eski kaydı
kalmış bir program sana önerilmez. Aşağıdaki şema akışın tamamını
gösterir — havuz 24&apos;ün altına düşerse filtrelerin nasıl
gevşetildiği, yine de yetmezse listenin eldeki programlarla, sana
açıkça belirtilerek nasıl kurulduğu da dahil.
</p>
<HuniSemasi />
</section>

View File

@@ -2,7 +2,7 @@
// 24'lük listenin gövdesi. Görsel dil sonuç sayfasındaki ham program
// tablosuyla birebir aynıdır (bkz. program-tablosu.tsx): aynı beyaz kart,
// aynı satır düzeni, aynı "Son 5 yıl" / "Taban sıra" sütunları. Buradaki fark
// aynı satır düzeni, aynı "Son N yıl" / "Taban sıra" sütunları. Buradaki fark
// içerik: satır sırası tercih numarası, risk sütunu Yapay Zeka'dan gelir ve
// satır açıldığında gerekçe/risk notu görünür. Program adı ve tabanlar herkese
// açık; Yapay Zeka katmanı kilitliBaslangic sonrasında ücretlidir (metin
@@ -27,6 +27,7 @@ import {
TableRow,
} from "@/components/ui/table";
import { bolumSayfaSlug, uniSayfaSlug } from "@/lib/slug";
import { VERI_YIL_SAYISI } from "@/lib/veri-yillari";
import type { RaporSonuc } from "@/features/rapor/types/rapor";
import {
DILIM_ETIKET,
@@ -47,7 +48,7 @@ const RISK_STIL: Record<RiskSeviyesi, { nokta: string; icon: typeof Rocket }> =
guvenli: { nokta: "bg-emerald-500", icon: ShieldCheck },
};
// Sütun sayısı: # · Program · Son 5 yıl · Risk · Taban sıra · detay oku
// Sütun sayısı: # · Program · Son N yıl · Risk · Taban sıra · detay oku
const SUTUN_SAYISI = 6;
/**
@@ -161,6 +162,19 @@ export function RaporListesi({
return iller.size === 1 ? [...iller][0] : null;
}, [rapor]);
// Sütun başlığı kaydın KENDİ penceresini söyler: eski raporların sıra
// geçmişi üretildiği günün yıl sayısını taşır (dizi doldurulmaz), başlık
// çizgide olmayan bir yılı vaat etmesin. Geçmişi hiç olmayan kayıtta
// bugünkü pencereye düşer.
const yilSayisi = useMemo(() => {
let enUzun = 0;
for (const t of rapor.tercihler) {
const n = rapor.programlar[t.programId]?.siraGecmisi?.length ?? 0;
if (n > enUzun) enUzun = n;
}
return enUzun > 0 ? enUzun : VERI_YIL_SAYISI;
}, [rapor]);
const satir = (t: RaporSonuc["tercihler"][number], index: number) => {
const p = rapor.programlar[t.programId];
const analizKilitli = index >= kilitIdx;
@@ -168,7 +182,7 @@ export function RaporListesi({
(adaySira != null ? riskHesapla(p?.efektifSira, adaySira) : null) ??
dilimdenRisk(t.dilim);
const stil = RISK_STIL[risk];
const seri = raporSiraSerisi(p?.siraGecmisi);
const seri = raporSiraSerisi(p?.siraGecmisi, p?.ilkYil);
const taban = p?.efektifSira ?? programEtkinSira(seri);
const acikMi = acik.has(t.sira);
const devlet = p?.unitur === "DEVLET";
@@ -366,7 +380,7 @@ export function RaporListesi({
<TableHead className="w-8 pl-4 pr-0">#</TableHead>
<TableHead>Program</TableHead>
<TableHead className="hidden text-right md:table-cell">
Son 5 yıl
Son {yilSayisi} yıl
</TableHead>
<TableHead>
<span className="hidden sm:inline">Yapay Zeka </span>Risk

View File

@@ -7,6 +7,7 @@ import { DILIM_ETIKET, RISK_ETIKET, dilimdenRisk } from "@/lib/risk";
import type { RaporParams, RaporSonuc } from "../types/rapor";
import { Skeleton } from "@/components/ui/skeleton";
import { YazdirButonu } from "./yazdir-butonu";
import { ILK_YIL, VERI_ARALIGI } from "@/lib/veri-yillari";
// Dilim sütunu: dilim adı + varsayılan riski. Üçüncü dilimde ikisi aynı
// kelime ("Güvenli") olduğu için parantez düşer — "Güvenli (Güvenli)" yazmaz.
@@ -29,6 +30,23 @@ export async function RaporYazdir() {
const rapor = satir.result as RaporSonuc;
const params = satir.params as RaporParams;
// Dipnottaki yıl aralığı raporun ÜRETİLDİĞİ veri penceresidir: eski kayıtlı
// rapor (5 yıllık geçmiş) güncel "2021–2026"yı iddia etmesin.
const gecmisler = Object.values(rapor.programlar).filter(
(p) => p.siraGecmisi && p.siraGecmisi.length > 0,
);
const veriAraligi =
gecmisler.length > 0
? (() => {
const ilk = Math.min(...gecmisler.map((p) => p.ilkYil ?? ILK_YIL));
const son = Math.max(
...gecmisler.map(
(p) => (p.ilkYil ?? ILK_YIL) + p.siraGecmisi!.length - 1,
),
);
return `${ilk}–${son}`;
})()
: VERI_ARALIGI;
const tarih = new Date(satir.updatedAt).toLocaleDateString("tr-TR", {
day: "numeric",
month: "long",
@@ -106,7 +124,7 @@ export async function RaporYazdir() {
<footer className="mt-6 border-t border-slate-300 pt-3 text-[10px] leading-relaxed text-slate-500">
<p>
Bu rapor KolayTercih tarafından resmî YÖK Atlas verisi (2021–2025
Bu rapor KolayTercih tarafından resmî YÖK Atlas verisi ({veriAraligi}{" "}
taban sıralamaları, kontenjan ve yerleşme istatistikleri) kullanılarak
üretilmiştir. Taban sıralamaları her yıl değişir; bu rapor yerleşme
garantisi vermez. Tercih listesinin ÖSYM sistemine girilmesi ve nihai

View File

@@ -105,7 +105,7 @@ export function TadimlikSatiri({
const p = satir.program;
const risk = riskHesapla(p.efektifSira, sira) ?? dilimdenRisk(satir.dilim);
const seri = raporSiraSerisi(p.siraGecmisi);
const seri = raporSiraSerisi(p.siraGecmisi, p.ilkYil);
const taban = p.efektifSira ?? programEtkinSira(seri);
const devlet = p.unitur === "DEVLET";
const girisUrl = `/giris?callback=${encodeURIComponent(sonucHref())}`;

View File

@@ -34,8 +34,13 @@ export type RaporSonuc = {
// ayıklamak için (bkz. features/rapor/rapor-queries.ts)
devMock?: boolean;
// Render için havuzdan zenginleştirilmiş program bilgisi
// efektifSira: COALESCE(sira2025, sira2024) — eski kayıtlı raporlarda yok
// siraGecmisi: [2021..2025] taban sıralamaları — deterministik trend oku
// efektifSira: üretim anındaki son yılın sırası, yoksa bir önceki yılınki —
// eski kayıtlı raporlarda yok
// siraGecmisi: [ilkYil..] taban sıralamaları — deterministik trend oku.
// Uzunluk üretim anındaki veri penceresidir (eski kayıtlarda 5 eleman);
// okuyan taraf diziyi doldurmaz, yılı `ilkYil + indeks` ile bulur.
// ilkYil: dizinin ilk elemanının yılı — eski kayıtlarda yok (okurken
// lib/veri-yillari ILK_YIL)
programlar: Record<
string,
{
@@ -45,6 +50,7 @@ export type RaporSonuc = {
unitur: string | null;
efektifSira?: number | null;
siraGecmisi?: (number | null)[];
ilkYil?: number;
}
>;
};

View File

@@ -26,6 +26,7 @@ import {
riskNotuMetni,
} from "../risk";
import { trendOzetiMetni } from "../trend";
import { ILK_YIL } from "../veri-yillari";
// Yapay zekâdan istenen küçük şema (kullanıcı kararı, 6 Ağu 2026): riskNotu
// ve trendOzeti gerçek sıra geçmişinden deterministik üretilir; dilim iskeleti
@@ -141,29 +142,27 @@ function normalizeEt(
function trendOzetiUret(p: AdayProgram): string {
// Cümle tadımlıkla ortak saf fonksiyondan gelir (lib/trend.ts)
return trendOzetiMetni([
p.sira2021,
p.sira2022,
p.sira2023,
p.sira2024,
p.sira2025,
]);
return trendOzetiMetni(p.siraGecmisi);
}
function riskNotuUret(p: AdayProgram, adaySira: number): string {
return riskNotuMetni(riskHesapla(p.sira2025 ?? p.sira2024, adaySira));
return riskNotuMetni(riskHesapla(p.efektifSira, adaySira));
}
/** Havuz programını render için gereken bilgiye indirger. */
function programBilgisi(p: AdayProgram): RaporSonuc["programlar"][string] {
const gecmis = [p.sira2021, p.sira2022, p.sira2023, p.sira2024, p.sira2025];
const gecmis = p.siraGecmisi;
const gecmisVar = gecmis.some((s) => s != null);
return {
isim: p.isim,
universite: p.universite,
il: p.il,
unitur: p.unitur,
efektifSira: p.sira2025 ?? p.sira2024,
siraGecmisi: gecmis.some((s) => s != null) ? gecmis : undefined,
efektifSira: p.efektifSira,
siraGecmisi: gecmisVar ? gecmis : undefined,
// Dizinin çapa yılı snapshot'a yazılır: kayıt, veri penceresi sonradan
// uzasa da kendi yıllarını bilir (eski kayıtlarda yok → okurken ILK_YIL).
ilkYil: gecmisVar ? ILK_YIL : undefined,
};
}
@@ -183,7 +182,7 @@ function mockRapor(
// Prod ile aynı hedef hesabı: mock, uyarlanmış dağılımı da birebir taklit eder
const hedef = dilimHedefiHesapla(havuz);
const toplamHedef = hedef.hayal + hedef.dengeli + hedef.garanti;
const efektif = (p: AdayProgram) => p.sira2025 ?? p.sira2024 ?? 0;
const efektif = (p: AdayProgram) => p.efektifSira ?? 0;
const dilimden = (dilim: AdayProgram["dilim"], adet: number) =>
havuz
.filter((p) => p.dilim === dilim)
@@ -293,7 +292,7 @@ export async function raporUret(
const havuzMap = new Map(havuz.map((p) => [p.id, p]));
const efektif = (id: string) => {
const p = havuzMap.get(id)!;
return p.sira2025 ?? p.sira2024 ?? Number.MAX_SAFE_INTEGER;
return p.efektifSira ?? Number.MAX_SAFE_INTEGER;
};
let secim: Record<Dilim, AiSecim[]> | null = null;

View File

@@ -3,6 +3,7 @@
import path from "node:path";
import Database from "better-sqlite3";
import { KATEGORILER, kategoriBul } from "./kategoriler";
import { EFEKTIF_SIRA_SQL, SON_YIL, YILLAR } from "./veri-yillari";
import {
PUAN_TURLERI,
type DilimKey,
@@ -27,12 +28,46 @@ export function getDb(): Database.Database {
return globalForDb.yokatlasDb;
}
// Yıl ekli kolonlar yalnız burada, `YILLAR`'dan üretilerek adlandırılır; DB
// dışına yılsız `Program` çıkar. SELECT_COLS ile okunan her satır
// `programSatiri` eşleyicisinden geçmelidir (ham satır `Program` değildir).
export const SELECT_COLS = `id, isim, universite, unitur, il, fakulte, tur, sure,
sira2025, sira2024, sira2023, sira2022, sira2021,
puan2025, kontenjan2025, yerlesen2025`;
${YILLAR.map((yil) => `sira${yil} AS s${yil}`).join(", ")},
puan${SON_YIL} AS puanSon, kontenjan${SON_YIL} AS kontenjanSon, yerlesen${SON_YIL} AS yerlesenSon`;
// 2025 sıralaması yoksa (az yerleşen/yeni program) 2024'e düşer
export const EFEKTIF_SIRA = "COALESCE(sira2025, sira2024)";
// Son yılın sıralaması yoksa (az yerleşen/yeni program) bir önceki yıla düşer.
// İfade indeks sözleşmesidir; tek kaynağı lib/veri-yillari.ts.
export const EFEKTIF_SIRA = EFEKTIF_SIRA_SQL;
/** SELECT_COLS ile okunan ham satır: sıra kolonları `s<yıl>` takma adıyla gelir. */
export type ProgramHamSatiri = Omit<
Program,
"siraGecmisi" | "efektifSira" | "siraSon"
> &
Record<`s${number}`, number | null>;
/** Ham DB satırını yılsız `Program`'a çevirir. */
export function programSatiri(row: ProgramHamSatiri): Program {
const siraGecmisi = YILLAR.map((yil) => row[`s${yil}`] ?? null);
const siraSon = siraGecmisi[siraGecmisi.length - 1] ?? null;
return {
id: row.id,
isim: row.isim,
universite: row.universite,
unitur: row.unitur,
il: row.il,
fakulte: row.fakulte,
tur: row.tur,
sure: row.sure,
siraGecmisi,
// EFEKTIF_SIRA ile aynı kural: son yıl, yoksa bir önceki yıl
efektifSira: siraSon ?? siraGecmisi[siraGecmisi.length - 2] ?? null,
siraSon,
puanSon: row.puanSon,
kontenjanSon: row.kontenjanSon,
yerlesenSon: row.yerlesenSon,
};
}
/**
* Kullanıcının başarı sıralamasına göre programları üç dilime ayırır.
@@ -112,18 +147,18 @@ export function dilimAra(
// hayal: sınıra en yakın (en büyük taban) önce; diğerleri en yakından uzağa
const yon = dilim === "hayal" ? "DESC" : "ASC";
const programlar = db
const satirlar = db
.prepare(
`SELECT ${SELECT_COLS} ${base} ORDER BY ${EFEKTIF_SIRA} ${yon} LIMIT ? OFFSET ?`
)
.all(...args, limit, offset) as Program[];
.all(...args, limit, offset) as ProgramHamSatiri[];
const toplam = (
db.prepare(`SELECT COUNT(*) AS adet ${base}`).get(...args) as {
adet: number;
}
).adet;
return { programlar, toplam };
return { programlar: satirlar.map(programSatiri), toplam };
}
export function searchByRank(

View File

@@ -1,5 +1,11 @@
import "server-only";
import { getDb, SELECT_COLS, EFEKTIF_SIRA } from "./db";
import {
getDb,
programSatiri,
SELECT_COLS,
EFEKTIF_SIRA,
type ProgramHamSatiri,
} from "./db";
import type { Program } from "@/types/yokatlas";
import { kategoriBul, type Kategori } from "./kategoriler";
import { uniAdiNormalize } from "./harita";
@@ -24,7 +30,7 @@ export type BolumOzet = {
kategoriler: Kategori[];
lisans: boolean;
onlisans: boolean;
/** MIN(COALESCE(sira2025, sira2024)) — en iyi (en küçük) taban sıralaması */
/** MIN(efektif sıra) — en iyi (en küçük) taban sıralaması */
enIyiSira: number | null;
};
@@ -131,13 +137,15 @@ export function getBolumBySlug(slug: string): BolumDetay | null {
if (!kayit) return null;
const placeholders = kayit.hamIsimler.map(() => "?").join(",");
const programlar = getDb()
const programlar = (
getDb()
.prepare(
`SELECT ${SELECT_COLS} FROM programs
WHERE isim IN (${placeholders})
ORDER BY (${EFEKTIF_SIRA} IS NULL), ${EFEKTIF_SIRA} ASC`,
)
.all(...kayit.hamIsimler) as (Program & { onlisans?: number })[];
.all(...kayit.hamIsimler) as ProgramHamSatiri[]
).map(programSatiri);
// onlisans SELECT_COLS içinde yok; tur üzerinden ayır (TYT = önlisans)
const lisansProgramlari = programlar.filter((p) => p.tur !== "TYT");
@@ -156,14 +164,14 @@ export function getBolumBySlug(slug: string): BolumDetay | null {
if (p.unitur === "DEVLET") devlet += 1;
else if (p.unitur) vakif += 1;
else bilinmeyenTur += 1;
const efektif = p.sira2025 ?? p.sira2024;
const efektif = p.efektifSira;
if (efektif != null) {
if (minSira == null || efektif < minSira) minSira = efektif;
if (maxSira == null || efektif > maxSira) maxSira = efektif;
}
if (p.puan2025 != null) {
if (minPuan == null || p.puan2025 < minPuan) minPuan = p.puan2025;
if (maxPuan == null || p.puan2025 > maxPuan) maxPuan = p.puan2025;
if (p.puanSon != null) {
if (minPuan == null || p.puanSon < minPuan) minPuan = p.puanSon;
if (maxPuan == null || p.puanSon > maxPuan) maxPuan = p.puanSon;
}
}
@@ -357,13 +365,15 @@ export function getUniversiteBySlug(slug: string): UniDetay | null {
if (!kayit) return null;
const placeholders = kayit.hamAdlar.map(() => "?").join(",");
const programlar = getDb()
const programlar = (
getDb()
.prepare(
`SELECT ${SELECT_COLS} FROM programs
WHERE universite IN (${placeholders})
ORDER BY fakulte, (${EFEKTIF_SIRA} IS NULL), ${EFEKTIF_SIRA} ASC`,
)
.all(...kayit.hamAdlar) as Program[];
.all(...kayit.hamAdlar) as ProgramHamSatiri[]
).map(programSatiri);
const gruplar = new Map<string, Program[]>();
let minSira: number | null = null;
@@ -373,11 +383,11 @@ export function getUniversiteBySlug(slug: string): UniDetay | null {
const liste = gruplar.get(anahtar);
if (liste) liste.push(p);
else gruplar.set(anahtar, [p]);
const efektif = p.sira2025 ?? p.sira2024;
const efektif = p.efektifSira;
if (efektif != null && (minSira == null || efektif < minSira)) {
minSira = efektif;
}
toplamKontenjan += p.kontenjan2025 ?? 0;
toplamKontenjan += p.kontenjanSon ?? 0;
}
const ozet = getAllUniversiteler().find((u) => u.slug === slug)!;

View File

@@ -8,6 +8,7 @@ import {
} from "@/types/yokatlas";
import type { RaporParams } from "@/features/rapor/types/rapor";
import { kategoriEslesir } from "./kategoriler";
import { SON_YIL, YILLAR } from "./veri-yillari";
export type Dilim = "hayal" | "dengeli" | "garanti";
@@ -155,6 +156,13 @@ export function havuzOlustur(params: RaporParams): HavuzSonuc {
return { havuz: dilimBasinaKirp(ilsizHam, PROMPT_LIMIT_DILIM), gevsetildi };
}
// Prompt'a veri penceresinin tamamı değil son 4 yılı gider (yeniden eskiye):
// pencere uzadıkça satır başına token sayısı sabit kalır.
const PROMPT_YIL_SAYISI = 4;
const PROMPT_YIL_INDEKSLERI = YILLAR.map((_, i) => i)
.slice(-PROMPT_YIL_SAYISI)
.reverse();
/** Prompt'a enjekte edilecek kompakt satır (token tasarrufu). */
export function havuzuKompaktJson(havuz: AdayProgram[]): string {
return JSON.stringify(
@@ -165,14 +173,16 @@ export function havuzuKompaktJson(havuz: AdayProgram[]): string {
unitur: p.unitur,
il: p.il,
dilim: p.dilim,
sira: {
y2025: p.sira2025,
y2024: p.sira2024,
y2023: p.sira2023,
y2022: p.sira2022,
},
kontenjan2025: p.kontenjan2025,
yerlesen2025: p.yerlesen2025,
// Anahtarlar yıl taşır (y<yıl>, kontenjan<yıl>): model rakamın hangi
// yıla ait olduğunu anahtardan okur.
sira: Object.fromEntries(
PROMPT_YIL_INDEKSLERI.map((i) => [
`y${YILLAR[i]}`,
p.siraGecmisi[i] ?? null,
]),
),
[`kontenjan${SON_YIL}`]: p.kontenjanSon,
[`yerlesen${SON_YIL}`]: p.yerlesenSon,
})),
);
}

View File

@@ -44,7 +44,7 @@ export function raporMaskele(rapor: RaporSonuc): MaskeliRapor {
// kilitli metinler istemciye hiç ulaşmaz.
uyarilar: rapor.uyarilar?.slice(0, 1) ?? [],
kilitliUyariSayisi: Math.max(0, (rapor.uyarilar?.length ?? 0) - 1),
// Program adı, üniversite ve 2021–2025 taban sıralamaları ham veridir.
// Program adı, üniversite ve geçmiş yılların taban sıralamaları ham veridir.
programlar: rapor.programlar,
// Kapsam (kaç program bulundu / dağılım uyarlandı mı) dürüstlük bilgisi:
// paketsiz kullanıcı da görür, maskelenmez.

View File

@@ -32,9 +32,10 @@ export const RISK_PIN_RENK: Record<RiskSeviyesi, string> = {
};
/**
* Deterministik risk: programın efektif taban sıralaması (COALESCE 2025→2024)
* adayın sıralamasıyla kıyaslanır. Eşikler db.ts'teki dilim aralıklarıyla
* birebir aynı (dengeli: [sira, 1.4×sira], garanti: (1.4×sira, ∞)).
* Deterministik risk: programın efektif taban sıralaması (son yılın sırası,
* yoksa bir önceki yılınki) adayın sıralamasıyla kıyaslanır. Eşikler
* db.ts'teki dilim aralıklarıyla birebir aynı (dengeli: [sira, 1.4×sira],
* garanti: (1.4×sira, ∞)).
*/
export function riskHesapla(
efektifSira: number | null | undefined,
@@ -72,7 +73,8 @@ export function dilimdenFarkliRiskEtiketi(
* Risk seviyesinin tek cümlelik açıklaması (rapor satırları + tadımlık).
* Ürün yerleşme olasılığı HESAPLAMAZ; yalnızca son taban sıralamasını adayın
* sırasıyla kıyaslar. Bu yüzden metin olasılık/ihtimal/"yerleşirsin" demez.
* "Geçen yıl" da denmez: efektif sıra 2025 yoksa 2024'e düşüyor.
* "Geçen yıl" da denmez: efektif sıra, son yılın verisi yoksa bir önceki
* yılınkine düşüyor.
*/
export function riskNotuMetni(risk: RiskSeviyesi | null): string {
switch (risk) {

View File

@@ -21,7 +21,7 @@ export const SITE_NAME = "KolayTercih";
* Yalnızca veri GERÇEKTEN değiştiğinde (ingest/refresh sonrası) elle güncelle;
* build zamanına bağlama — her deploy'da değişen lastmod sinyali değersizleşir.
*/
export const KATALOG_VERI_TARIHI = "2026-08-09";
export const KATALOG_VERI_TARIHI = "2026-09-21";
type SayfaMetadataGirdi = {
title: string;

View File

@@ -10,6 +10,7 @@ import type { Dilim } from "./rapor-havuzu";
import type { RaporSonuc } from "@/features/rapor/types/rapor";
import { dilimdenRisk, riskHesapla, riskNotuMetni } from "./risk";
import { trendOzetiMetni } from "./trend";
import { VERI_YIL_SAYISI } from "./veri-yillari";
/** Sihirbaz kategorisi seçilmemiş kullanıcı için kova×tür başına fallback satır. */
export const GENEL_KATEGORI = "genel";
@@ -54,16 +55,41 @@ export type TadimlikTip = "genel" | "devlet" | "vakif";
* gerekce, satırın servis edilen TEK LLM metnidir (batch'te temsili sıraya
* göre yazıldı). Ürün yerleşme olasılığı hesaplamaz; bu yüzden olasılık /
* ihtimal / şans / garanti / kesin / yüzde / "yerleşirsin" diyen gerekçe
* servis edilmez. Eski batch'in "son 4 yıl" kalıbı da takılır (veri 5 yılı
* kapsıyor, bkz. veri-kapsami.ts). Kalıp küçük harfledir: metin önce Türkçe
* kurallarıyla küçültülür ("İhtimal" → "ihtimal"; düz /i bayrağı İ/ı'yı
* eşlemez).
* servis edilmez. Kalıp küçük harfledir: metin önce Türkçe kurallarıyla
* küçültülür ("İhtimal" → "ihtimal"; düz /i bayrağı İ/ı'yı eşlemez).
*/
const YASAK_GEREKCE_DILI =
/olasılı|ihtimal|şans|garanti|kesin|yüzde|yerleşirsin|yerleşebil|son (4|dört) yıl/;
/olasılı|ihtimal|şans|garanti|kesin|yüzde|yerleşirsin|yerleşebil/;
// Bayat batch kalıbı: gerekçe "son N yıl" diyorsa N, verinin bugün kapsadığı
// yıl sayısı (VERI_YIL_SAYISI) olmalı. Veri penceresi uzayınca eski batch'in
// "son 5 yıl"ı da, daha eskisinin "son 4 yıl"ı da kendiliğinden takılır.
const SON_N_YIL = /son (\d+|[a-zçğıöşü]+) yıl/g;
const SAYI_SOZCUKLERI: Record<string, number> = {
bir: 1,
iki: 2,
üç: 3,
dört: 4,
beş: 5,
altı: 6,
yedi: 7,
sekiz: 8,
dokuz: 9,
on: 10,
};
function bayatYilSayisiVar(kucukMetin: string): boolean {
for (const [, ham] of kucukMetin.matchAll(SON_N_YIL)) {
const n = /^\d+$/.test(ham) ? Number(ham) : SAYI_SOZCUKLERI[ham];
// Sayı olmayan sözcük ("son birkaç yıl") yıl sayısı iddiası değildir
if (n != null && n !== VERI_YIL_SAYISI) return true;
}
return false;
}
export function gerekceServisEdilebilir(gerekce: string): boolean {
return !YASAK_GEREKCE_DILI.test(gerekce.toLocaleLowerCase("tr-TR"));
const kucuk = gerekce.toLocaleLowerCase("tr-TR");
return !YASAK_GEREKCE_DILI.test(kucuk) && !bayatYilSayisiVar(kucuk);
}
/**
@@ -108,10 +134,12 @@ export async function tadimlikSec(
// aynı formülden adayın gerçek sırasına göre; trend, rapordakiyle aynı
// saf fonksiyondan programın sıra geçmişinden okuma anında üretilir —
// metin rozetle çelişemez, "son 4 yıl" gibi bayat kalıp taşınmaz.
// Snapshot'ın sıra dizisi üretildiği günün penceresidir (eski
// satırlarda 5 eleman); yıllar dizinin kendi çapasından okunur.
riskNotu: riskNotuMetni(
riskHesapla(program.efektifSira, sira) ?? dilimdenRisk(satir.dilim),
),
trendOzeti: trendOzetiMetni(program.siraGecmisi),
trendOzeti: trendOzetiMetni(program.siraGecmisi, program.ilkYil),
program,
};
}

View File

@@ -4,12 +4,7 @@
// düzeyine indi"); yıl aralığı sabit bir "son N yıl" kalıbından değil,
// serideki dolu yıllardan gelir.
/**
* Sıra geçmişi dizisinin ilk elemanının yılı. Dizi her yerde 2021'den başlar
* (bkz. programBilgisi, raporSiraSerisi); yeni veri yılı sona eklenir, bu
* çapa değişmez.
*/
const ILK_VERI_YILI = 2021;
import { ILK_YIL } from "./veri-yillari";
/** 45000 -> "45B", 1_200_000 -> "1,2M" (şemadaki öğrenci dili). */
function siraKisa(n: number): string {
@@ -21,7 +16,10 @@ function siraKisa(n: number): string {
}
/**
* [2021..] taban sıralaması dizisinden tek cümlelik trend özeti.
* [ilkYil..] taban sıralaması dizisinden tek cümlelik trend özeti. Dizi
* `ilkYil`'a demirlidir (varsayılan: veri penceresinin ilk yılı); yeni veri
* yılı sona eklenir, bu yüzden eski kayıtların daha kısa dizileri de doğru
* yılları verir. `ilkYil`, snapshot kendi çapasını taşıyorsa ondan gelir.
*
* Cümle yalnızca OLANI betimler: ilk ve son dolu yılın tabanını kıyaslar
* (±%10 eşiği). "Zorlaşıyor / kolaylaşıyor" gibi geleceğe dönük yargı
@@ -30,22 +28,23 @@ function siraKisa(n: number): string {
*/
export function trendOzetiMetni(
gecmis: readonly (number | null | undefined)[] | undefined,
ilkYil: number = ILK_YIL,
): string {
const seri: Array<[number, number]> = [];
(gecmis ?? []).forEach((sira, i) => {
if (sira != null) seri.push([ILK_VERI_YILI + i, sira]);
if (sira != null) seri.push([ilkYil + i, sira]);
});
if (seri.length < 2) {
return "Karşılaştırma için yeterli geçmiş yıl verisi yok; trendi temkinli oku.";
}
const [ilkYil, ilkSira] = seri[0];
const [basYil, ilkSira] = seri[0];
const [sonYil, sonSira] = seri[seri.length - 1];
const oran = (sonSira - ilkSira) / ilkSira;
if (oran <= -0.1) {
return `${ilkYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine indi; taban sıkılaştı.`;
return `${basYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine indi; taban sıkılaştı.`;
}
if (oran >= 0.1) {
return `${ilkYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine çıktı; taban gevşedi.`;
return `${basYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine çıktı; taban gevşedi.`;
}
return `${ilkYil}-${sonYil} arasında taban sıralaması ${siraKisa(sonSira)} düzeyinde stabil seyretti.`;
return `${basYil}-${sonYil} arasında taban sıralaması ${siraKisa(sonSira)} düzeyinde stabil seyretti.`;
}

View File

@@ -1,6 +1,6 @@
/**
* Taban sıralaması geçmişinin kapsadığı yıl sayısı. Veritabanında
* sira2021–sira2025 kolonları var (5 yıl); "son N yıl" diyen tüm metinler bu
* sabitten okur. Yeni veri yılı eklenince (ör. sira2026) burayı güncelle.
* Taban sıralaması geçmişinin kapsadığı yıl sayısı; "son N yıl" diyen tüm
* metinler bu sabitten okur. Değer yıl kapsamının tek kaynağından
* (lib/veri-yillari.ts) türetilir — yeni veri yılı eklenince burası değişmez.
*/
export const VERI_YIL_SAYISI = 5;
export { VERI_YIL_SAYISI } from "./veri-yillari";

38
src/lib/veri-yillari.ts Normal file
View File

@@ -0,0 +1,38 @@
/**
* Yerleştirme verisinin yıl kapsamı — TEK KAYNAK. Saf ve import'suz: client,
* server ve `scripts/` aynı dosyayı okur. Yeni yıl verisi yüklenince yalnız
* `SON_YIL` değişir (+ `pnpm refresh -- --yil <yıl>` ile DB inşası).
*/
/** Veri penceresinin ilk yılı. Kayıtlı rapor/tadımlık `siraGecmisi` dizileri
* bu yıla demirlidir; pencere kaydırılmaz, uzar. */
export const ILK_YIL = 2021;
/** En son yerleştirme yılı (DB'de `sira${SON_YIL}` kolonları dolu). */
export const SON_YIL = 2026;
export const ONCEKI_YIL = SON_YIL - 1;
/** [2021, …, SON_YIL] — `siraGecmisi` dizileriyle indeks hizalı. */
export const YILLAR: readonly number[] = Array.from(
{ length: SON_YIL - ILK_YIL + 1 },
(_, i) => ILK_YIL + i,
);
/** Bu veriyle tercih yapacak öğrencilerin yılı. */
export const TERCIH_YILI = SON_YIL + 1;
/** Sayfa başlıklarında görünen yıl ("… Taban Puanları 2026"). */
export const BASLIK_YILI = SON_YIL;
export const VERI_YIL_SAYISI = YILLAR.length;
/** "2021–2026" */
export const VERI_ARALIGI = `${ILK_YIL}–${SON_YIL}`;
/**
* Efektif sıra: son yılın sırası, yoksa bir önceki yılınki. İNDEKS SÖZLEŞMESİ —
* SQLite ifade indeksi metinsel eşleşmeyle çalışır; sorgular ve indeks
* betikleri bu dizgiyi birebir (virgülden sonra tek boşluk) kullanmalıdır.
*/
export const EFEKTIF_SIRA_SQL = `COALESCE(sira${SON_YIL}, sira${ONCEKI_YIL})`;

View File

@@ -11,14 +11,16 @@ export type Program = {
fakulte: string | null;
tur: string;
sure: number | null;
sira2025: number | null;
sira2024: number | null;
sira2023: number | null;
sira2022: number | null;
sira2021: number | null;
puan2025: number | null;
kontenjan2025: number | null;
yerlesen2025: number | null;
/** Taban sıralamaları; `YILLAR` (lib/veri-yillari) ile indeks hizalı. */
siraGecmisi: (number | null)[];
/** Son yılın sırası; o yıl verisi yoksa bir önceki yılınki. */
efektifSira: number | null;
// "Son" = en son yerleştirme yılı (SON_YIL). Alan adları bilinçli yılsız:
// yeni veri yılı yüklenince tip ve tüketiciler değişmez.
siraSon: number | null;
puanSon: number | null;
kontenjanSon: number | null;
yerlesenSon: number | null;
};
export const PUAN_TURLERI = {
@@ -42,11 +44,15 @@ export type RankResults = {
};
/**
* /sonuc ham tablosunun satırı: tablo `fakulte` ve `puan2025`'i hiç
* /sonuc ham tablosunun satırı: tablo `fakulte`, `puanSon` ve `siraSon`'u
* (dizinin son elemanı; tablo `efektifSira` + `siraGecmisi` ile çalışır) hiç
* kullanmadığı için bu alanlar client'a serileştirilmez (satır başına en
* uzun string alanlardan biri fakulte). Katalog sayfaları tam Program kullanır.
*/
export type ProgramListeSatiri = Omit<Program, "fakulte" | "puan2025">;
export type ProgramListeSatiri = Omit<
Program,
"fakulte" | "puanSon" | "siraSon"
>;
export type RankResultsListe = {
hayal: ProgramListeSatiri[];