/** * 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. * * Kimliği onarılmış satırlar (refresh --kimlik-onar; bkz. kimlik.ts): devir * yılından ÖNCEKİ yıllar hiçbir kaynaktan doldurulmaz — o yıllarda bu kod başka * programa aitti ve API'nin kod bazlı `…N` alanları o programın değerini verir. * * 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 { onarimKayitlari } from "./kimlik"; import { gecmisAlanlari, hamOku, sonHamDosya } from "./refresh"; import { argOku, bayrakVar, calistir, dbAc, dbArgumani, dur, kolonAdlari, tabloVar, yilArgumani, YIL_OLCULERI, } from "./yokatlas-goc"; type Satir = Record & { 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(); // "kaynak|kolon" → adet const farkli = new Map(); const say = (m: Map, 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; // id → devir yılı: bu yıldan önceki kolonlar o satırda BİLEREK boş. const devirYillari = new Map(); for (const [id, k] of onarimKayitlari(db)) devirYillari.set(id, k.devirYili); let devirOncesiAtlanan = 0; const devirOncesi = (id: string, y: number): boolean => { const devir = devirYillari.get(id); if (devir == null || y >= devir) return false; devirOncesiAtlanan++; return true; }; const setCache = new Map>(); 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) { if (devirOncesi(hedef.id, y)) continue; 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 = { 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; if (devirOncesi(hedef.id, 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(); 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(); 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(" ; ")}`); } if (devirYillari.size) { console.log( `Kimliği onarılmış ${devirYillari.size} satırda devir öncesi yıl BİLEREK boş bırakıldı ` + `(atlanan yıl denemesi: ${devirOncesiAtlanan}).` ); } console.log(`\nDoldurulan NULL alanlar${kuru ? " (kuru — yazılmadı)" : ""}:`); const toplamlar = new Map(); 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);