Files
kolaytercih/scripts/gecmis-doldur.ts
bilalgursen a773ac7beb feat(veri): 2026 yükleme hattı — yıl kilitli refresh, geçmiş doldurma, arşiv, kalite kapısı (P-A)
- yokatlas-goc: idempotent şema göçü (yıl kolonları, son_kilavuz_yili, eski_kod,
  programs_arsiv, veri_meta) + betiklerin ortak yardımcıları
- refresh: --yil/--db zorunlu, YIL KİLİDİ (…1 = DB sira<yil-1> ≥%95 ve eksiz ≠),
  yıl asla r.yil'den türetilmez; ham veri data/ham/*.jsonl.gz, --ham ile API'siz
  tekrar; benzersiz kod = totalElements; tek transaction + önceki yıl sağlaması;
  yeni programda …1/2/3 → üç önceki yıl; efektif indeks EFEKTIF_SIRA_SQL ile
- gecmis-doldur: yalnız NULL alan; API …1/2/3 → eski_kod → kanıtlı 1:1 ad eşleşmesi
- eski-arsivle: güncel kılavuzda olmayanlar → programs_arsiv, netler yeni koda; --kuru
- detay: netler bağımsız + yıl sağlaması; kırılım yalnız --kirilim, alan yoksa durur,
  mevcut değerin üstüne NULL yazılmaz; `yil - 1` varsayımı kalktı
- veri-kalite: salt okunur kalite kapısı, zorunlu kontrol kalırsa çıkış kodu 1
- docs/veri/yillik-guncelleme.md: adım adım runbook + geri alma

DB commit'lenmedi (arşivleme kararı [Bilal]).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 17:03:01 +03:00

303 lines
12 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.
/**
* 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);