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>
This commit is contained in:
302
scripts/gecmis-doldur.ts
Normal file
302
scripts/gecmis-doldur.ts
Normal 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);
|
||||
Reference in New Issue
Block a user