Files
kolaytercih/scripts/veri-kalite.ts
bilalgursen a3cb058a3b
All checks were successful
Deploy / deploy (push) Successful in 7m45s
feat: update app.db and BACKLOG.md with new decisions and adjustments
- Updated the app.db file to reflect recent changes in the application state.
- Modified BACKLOG.md to include new entries regarding the removal of certain wizard steps, adjustments to priority chips, and the introduction of special quota conditions for candidates.
- Added details on the decision to link priority chips to data sources and clarified the implications for user experience and data handling.
2026-10-06 17:17:49 +03:00

468 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 <dosya>]
*
* --ham <dosya> 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 <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 { 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<string, unknown> | 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<string, unknown> | undefined;
const s = yeniGetir.get(id) as Record<string, unknown> | 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<string, { unitur: string | null; program: number }>();
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(", ")}`
);
}