/** * 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] [--ham ] * * --ham Ham API çekimi (yoksa data/ham altındaki en yenisi). Verilirse * KİMLİK kontrolü koşar: her DB satırının puan türü / önlisans / * baz ad slug'ı aynı koddaki API kaydıyla uyuşmalı (0 beklenir; * uyuşmayan = kılavuz kodu başka programa devredilmiş, satır eski * adıyla yeni programın sırasını gösteriyor → refresh --kimlik-onar). * --onceki 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 { kimlikFarklari, onarimKayitlari, yilKolonlari, type DbKimlik } from "./kimlik"; import { hamOku, sonHamDosya, TUR_MAP } from "./refresh"; import { KKTC_ILLERI } from "./unitur-onar"; import { argOku, bayrakVar, calistir, dbAc, dbArgumani, dur, kolonAdlari, metaOku, tabloVar, yilArgumani, } from "./yokatlas-goc"; // URAP kontrolleri (dosya sonundaki urapKontrolleri) — salt okunur sabit. import type { Db } from "./yokatlas-goc"; import { uniSayfaSlug } from "../src/lib/slug"; import { URAP_DONEM, URAP_SIRA, URAP_TOPLAM } from "../src/lib/urap-siralama"; 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`); // 6b. Kontenjan türü kırılımı (detay --kirilim; `…1` alanları bir önceki // yıla yazılır, bu yüzden en yeni kırılım yılı yıl-1 olabilir). Türlerin // toplamı genel kontenjanı aşamaz; yerleşen toplamı yıl toplamıyla uyuşmalı. // Bilinen fark: refresh.ts `yerlesen()` toplamına y34kY dahil değil; bu // kontrol o farkı görünür kılar (BACKLOG, 6 Eki 2026). const kirilimYili = [yil, onceki].find((y) => kolonlar.has(`gk${y}`) && kolonlar.has(`y34k${y}`)); if (kirilimYili == null) { bilgi("kontenjan türleri", false, "kırılım kolonu yok (detay --kirilim koşulmamış); atlandı"); } else { const ky = kirilimYili; const turToplamAsan = sayi( `SELECT COUNT(*) AS n FROM programs WHERE kontenjan${ky} IS NOT NULL AND COALESCE(obk${ky},0) + COALESCE(sgy${ky},0) + COALESCE(dprm${ky},0) + COALESCE(y34k${ky},0) > kontenjan${ky}` ); zorunlu( `kontenjan türleri ≤ kontenjan${ky}`, turToplamAsan === 0, `özel kontenjan toplamı genel kontenjanı aşan satır: ${turToplamAsan}${turToplamAsan ? ` ör. ${ornek(`SELECT id FROM programs WHERE COALESCE(obk${ky},0)+COALESCE(sgy${ky},0)+COALESCE(dprm${ky},0)+COALESCE(y34k${ky},0) > kontenjan${ky} LIMIT 3`)}` : ""}` ); const kirilimli = sayi(`SELECT COUNT(*) AS n FROM programs WHERE gkY${ky} IS NOT NULL AND yerlesen${ky} IS NOT NULL`); const yerlesenUyumsuz = sayi( `SELECT COUNT(*) AS n FROM programs WHERE gkY${ky} IS NOT NULL AND yerlesen${ky} IS NOT NULL AND COALESCE(gkY${ky},0) + COALESCE(obkY${ky},0) + COALESCE(sgyY${ky},0) + COALESCE(dprmY${ky},0) + COALESCE(y34kY${ky},0) <> yerlesen${ky}` ); bilgi( `yerlesen${ky} = türlerin yerleşen toplamı`, kirilimli > 0 && yerlesenUyumsuz / kirilimli > 0.02, `uyuşmayan ${yerlesenUyumsuz}/${kirilimli} (y34kY refresh toplamında yok; fark o kadarsa beklenen)` ); } // 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}`); // 9b. Kimlik: kod aynı, program aynı mı? (ham çekim gerekir) const hamArg = argOku("ham"); const hamDosya = hamArg ? path.resolve(process.cwd(), hamArg) : sonHamDosya(yil, path.resolve(process.cwd(), "data", "ham")); const onarimlar = onarimKayitlari(db); if (hamDosya) { const { kayitlar } = await hamOku(hamDosya, yil); const getir = db.prepare("SELECT id, isim, tur, onlisans, fakulte, sure, universite FROM programs WHERE id = ?"); const uyumsuz: string[] = []; let kiyaslanan = 0; let ekFarki = 0; for (const r of kayitlar) { if (!r.kilavuzKodu) continue; const satir = getir.get(String(r.kilavuzKodu)) as (DbKimlik & { universite: string }) | undefined; if (!satir) continue; kiyaslanan++; const farklar = kimlikFarklari(satir, r, TUR_MAP[r.puanTuru?.trim() ?? ""]); if (farklar.length) { uyumsuz.push(`${satir.id}[${farklar.join("+")}] DB="${satir.isim}"/${satir.tur}/${satir.onlisans} API="${r.birimAdi}"/${r.puanTuru}/${r.birimTuruId === 47 ? 1 : 0} (${satir.universite})`); } else if (satir.isim !== r.birimAdi) { ekFarki++; } } zorunlu( "kimlik (tur/önlisans/baz ad = API)", uyumsuz.length === 0, `${kiyaslanan} satır kıyaslandı, uyumsuz: ${uyumsuz.length}${uyumsuz.length ? ` ör. ${uyumsuz.slice(0, 3).join(" ; ")}` : ""} [${path.basename(hamDosya)}]` ); bilgi("isim eki (burs/dil) API'den farklı", false, `${ekFarki} satır — kimlik aynı, DB adı eksiz/farklı yazımlı (aynı üniversitede adaş varyantlar ayırt edilemez)`); } else { bilgi("kimlik", true, "ATLANDI: --ham verilmedi ve data/ham altında çekim yok; kod devri denetlenemedi"); } if (onarimlar.size) { // Onarılan satırda devir öncesi hiçbir yıl kolonu dolu kalmış olamaz. const yk = yilKolonlari(db); let sizinti = 0; const satirGetir = db.prepare("SELECT * FROM programs WHERE id = ?"); for (const [id, k] of onarimlar) { const s = satirGetir.get(id) as Record | undefined; if (!s) continue; if (yk.some((c) => c.yil < k.devirYili && s[c.kolon] != null)) sizinti++; } zorunlu("kimliği onarılanlar: devir öncesi boş", sizinti === 0, `${onarimlar.size} onarılmış satırın ${sizinti} tanesinde devir öncesi yıl yeniden dolmuş`); } // 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. // Kimliği onarılan satırlarda devir öncesi yıllar BİLEREK NULL'landı; onlar // ayrı sayılır: devir yılı ve sonrası yine birebir korunmuş olmalı. const onarilanJson = JSON.stringify([...onarimlar.keys()]); const degisen = sayi( `SELECT COUNT(*) AS n FROM eski.programs e JOIN ${simdi} s ON s.id = e.id WHERE e.id NOT IN (SELECT value FROM json_each(?)) AND (${ortak.map((k) => `(e.${k} IS NOT NULL AND e.${k} IS NOT s.${k})`).join(" OR ")})`, onarilanJson ); zorunlu("önceki DB: geçmiş yıllar korunmuş", degisen === 0, `${ortak.length} kolonda dolu değeri değişen/silinen satır: ${degisen} (kimliği onarılan ${onarimlar.size} satır hariç)`); if (onarimlar.size) { let bozulan = 0; const eskiGetir = db.prepare("SELECT * FROM eski.programs WHERE id = ?"); const yeniGetir = db.prepare("SELECT * FROM main.programs WHERE id = ?"); for (const [id, k] of onarimlar) { const e = eskiGetir.get(id) as Record | undefined; const s = yeniGetir.get(id) as Record | undefined; if (!e || !s) continue; const korunmali = ortak.filter((kolon) => Number(kolon.slice(-4)) >= k.devirYili); if (korunmali.some((kolon) => e[kolon] != null && e[kolon] !== s[kolon])) bozulan++; } zorunlu("önceki DB: onarılanlarda devir sonrası korunmuş", bozulan === 0, `devir yılı ve sonrasında değeri değişen onarılmış satır: ${bozulan}/${onarimlar.size}`); } 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)}`); } // 13. Aynı kurumun iki yazımı arasında unitur çelişkisi — BAZ AD üzerinden. // unitur-onar.ts'in eski çelişki koruması GROUP BY universite (TAM ad) ile // çalıştığı için "X ÜNİVERSİTESİ" ve "X ÜNİVERSİTESİ (ÜSKÜP-MAKEDONYA)" iki // ayrı kurum sayılıyor, aralarındaki DEVLET↔YURTDISI KAMU çelişkisi hiç // görünmüyordu. Baz ad = sondaki parantezli ek atılmış hâl. const BAZ_AD = "TRIM(CASE WHEN instr(universite, '(') > 0 THEN substr(universite, 1, instr(universite, '(') - 1) ELSE universite END)"; // 13 ve 14 arşivi de tarar: kapanan programlar `programs_arsiv`'e olduğu gibi // taşındığı için yanlış etiket orada da duruyordu (BACKLOG #21). Bugün `src/` // tarafında arşivi okuyan SQL yok, ama iki tablo ayrışırsa denetim bunu // görmez ve tablo ileride okunduğunda sızıntı geri gelir. const KURUMLAR = tabloVar(db, "programs_arsiv") ? `(SELECT universite, unitur, il FROM programs UNION ALL SELECT universite, unitur, il FROM programs_arsiv)` : "programs"; const bazAdCeliskileri = `(SELECT ${BAZ_AD} AS bazAd, GROUP_CONCAT(DISTINCT unitur) AS turler, COUNT(*) AS adet, MAX(unitur = 'DEVLET') <> MIN(unitur = 'DEVLET') AS devletBolunmus FROM ${KURUMLAR} WHERE unitur IS NOT NULL GROUP BY 1 HAVING COUNT(DISTINCT unitur) > 1)`; const bolunmus = ornek(`SELECT bazAd, turler, adet FROM ${bazAdCeliskileri} WHERE devletBolunmus`); zorunlu( "baz ad başına tek unitur", bolunmus === "", bolunmus === "" ? "aynı kurumun iki yazımı devlet/vakıf filtresini bölmüyor" : `devlet filtresine giren, vakıf filtresinden düşen kurum: ${bolunmus} → scripts/unitur-onar.ts KURUM_DUZELT` ); const esdegerFark = ornek(`SELECT bazAd, turler, adet FROM ${bazAdCeliskileri} WHERE NOT devletBolunmus`); bilgi("baz ad unitur farkı (devlet/vakıf ayrımını bölmeyen)", false, esdegerFark || "yok"); // 14. DEVLET etiketli ama hiçbir programı Türkiye ilinde olmayan kurum. // 13'ün kör noktası: kurumun İKİ yazımı da yanlış etiketliyse çelişki yoktur. // Türkiye illerinde tire geçmez; yurt dışı iller tireli (BİŞKEK-KIRGIZİSTAN…), // KKTC illeri KKTC_ILLERI'nde. İli hiç olmayan kurumlar (MSB/İçişleri) hariç. const yurtDisiDevlet = ornek( `SELECT universite, GROUP_CONCAT(DISTINCT il), COUNT(*) FROM ${KURUMLAR} WHERE unitur = 'DEVLET' GROUP BY universite HAVING SUM(il IS NOT NULL) > 0 AND SUM(il NOT LIKE '%-%' AND il NOT IN (${KKTC_ILLERI.map(() => "?").join(",")})) = 0`, ...KKTC_ILLERI ); zorunlu( "DEVLET etiketi Türkiye ili ile destekli", yurtDisiDevlet === "", yurtDisiDevlet === "" ? "DEVLET etiketli her kurumun en az bir programı Türkiye ilinde" : `tüm programları KKTC/yurt dışında olduğu hâlde DEVLET: ${yurtDisiDevlet}` ); urapKontrolleri(db, zorunlu, bilgi); 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); /** * URAP Türkiye sıralaması sabiti (`src/lib/urap-siralama.ts`) denetimi. * Amaç: yıllık tazelemede eşleşmeyen bir URAP satırı SESSİZCE DÜŞMESİN — * düşerse üniversitenin adı değişmiş demektir ve o sayfada sıra kaybolur. * DB'ye yazmaz; sabit ile DB'deki üniversite slug'larını karşılaştırır. */ function urapKontrolleri( db: Db, zorunlu: (ad: string, tamam: boolean, detay: string) => void, bilgi: (ad: string, sorunlu: boolean, detay: string) => void ) { const kayitlar = Object.entries(URAP_SIRA); // (1) Sabitteki her slug DB'de karşılık bulmalı — bulamayan = ad değişikliği. const dbToplam = new Map(); const satirlar = db .prepare("SELECT universite, unitur, COUNT(*) AS n FROM programs GROUP BY 1, 2") .all() as { universite: string; unitur: string | null; n: number }[]; for (const r of satirlar) { const slug = uniSayfaSlug(r.universite); if (!slug) continue; const kayit = dbToplam.get(slug) ?? { unitur: null, program: 0 }; kayit.program += r.n; if (!kayit.unitur && r.unitur) kayit.unitur = r.unitur; dbToplam.set(slug, kayit); } const karsiliksiz = kayitlar.filter(([slug]) => !dbToplam.has(slug)); zorunlu( "URAP: her sıra bir üniversite sayfasına bağlı", karsiliksiz.length === 0, karsiliksiz.length === 0 ? `${kayitlar.length} slug'ın tamamı DB'de var` : `DB'de karşılığı olmayan (ad değişmiş olabilir): ${karsiliksiz.map(([slug, v]) => `${slug}(#${v.sira})`).join(", ")}` ); // (2) Kayıt sayısı payda ile aynı olmalı — payda kartta gösteriliyor. zorunlu( "URAP: kayıt sayısı = URAP_TOPLAM", kayitlar.length === URAP_TOPLAM, `kayıt ${kayitlar.length}, URAP_TOPLAM ${URAP_TOPLAM} (dönem ${URAP_DONEM})` ); // (3) Sıralar 1..URAP_TOPLAM; tekrarsız ve boşluksuz. const siralar = kayitlar.map(([, v]) => v.sira).sort((a, b) => a - b); const bozukSira = siralar.findIndex((s, i) => s !== i + 1); zorunlu( "URAP: sıralar 1..N tekrarsız ve boşluksuz", bozukSira === -1 && siralar.length === URAP_TOPLAM, bozukSira === -1 ? `1..${siralar.length} kesintisiz` : `${bozukSira + 1}. sırada beklenen ${bozukSira + 1}, gelen ${siralar[bozukSira]}` ); // (4) Puan sıraya göre azalmalı — sıra 1 en yüksek puan. const sirali = [...kayitlar].sort((a, b) => a[1].sira - b[1].sira); const bozukPuan = sirali.findIndex( (k, i) => i > 0 && sirali[i - 1][1].puan < k[1].puan ); zorunlu( "URAP: puanlar sıraya göre azalan", bozukPuan === -1, bozukPuan === -1 ? `${sirali[0][1].puan} → ${sirali[sirali.length - 1][1].puan}` : `#${sirali[bozukPuan][1].sira} (${sirali[bozukPuan][0]}) puanı bir önceki sıradan yüksek` ); // (5) Kapsama boşluğu: URAP yalnız Türkiye'yi sıralar, KKTC/yurt dışı ve // yalnız MYO'su olan kurumlar listede olmaz — bu normaldir. Ama büyük bir // Türkiye üniversitesinin düşmesi neredeyse her zaman ad değişikliğidir. const disarida = [...dbToplam.entries()].filter(([slug]) => !(slug in URAP_SIRA)); const turkiyeBuyuk = disarida.filter( ([, v]) => (v.unitur === "DEVLET" || v.unitur === "VAKIF") && v.program >= 150 ); bilgi( "URAP: kapsama boşluğu", turkiyeBuyuk.length > 0, turkiyeBuyuk.length === 0 ? `URAP dışında ${disarida.length} kurum (KKTC/yurt dışı, MYO ve yeni üniversiteler); 150+ programlı Türkiye üniversitesi yok` : `URAP dışında kalan 150+ programlı Türkiye üniversitesi (ad değişmiş olabilir): ${turkiyeBuyuk.map(([slug, v]) => `${slug}(${v.program})`).join(", ")}` ); }