Files
kolaytercih/scripts/gecmis-doldur.ts
bilalgursen 2ff8fe584b fix(veri): kimlik kilidi — kodu başka programa devredilmiş satırlar yakalanır ve onarılır
Kılavuz kodu başka programa devredildiğinde (ör. 209210292: Diş Hekimliği SAY
lisans → İlk ve Acil Yardım TYT önlisans) refresh yalnız koda bakıp yıl
kolonlarını yazıyordu; satır eski adıyla yeni programın sırasını gösteriyordu.

- scripts/kimlik.ts (yeni): kimlik = puan türü + önlisans + baz ad slug'ı.
  Devir yılı kanıt zinciriyle bulunur (API …1/…2/…3 sıra/puanı DB ile eşleşen
  ardışık yıllar + netler.puanTuru); API geçmişi kod bazlı ve kirli olduğu
  için (gk2, bazı basariSirasi3) zincirsiz güvenilmez. Yalnız adı değişen ve
  API penceresinin tamamında sürekliliği olan satır "ad değişimi"dir, geçmişi
  korunur.
- refresh: uyumsuz satır varsa listeler ve HİÇBİR ŞEY yazmadan durur;
  --kimlik-onar ile isim/tur/onlisans/fakulte/sure API'den yazılır, devir
  öncesi tüm yıl kolonları NULL'lanır, eski kimlik + geçmişi programs_arsiv'e
  <kod>~<yıl> id'siyle kopyalanır, eski yılların netleri oraya taşınır,
  veri_meta'ya kimlik_onar:<kod> kaydı düşer. Önceki-yıl sağlaması onarılanlar
  dışındaki satırlar için aynen geçerli. --isim-esitle (varsayılan kapalı):
  kimliği aynı, yalnız burs/dil eki farklı satırlarda isim API'den.
- gecmis-doldur: onarılmış satırda devir öncesi yılları hiçbir kaynaktan
  doldurmaz (yoksa API'nin kod bazlı …N alanları eski programı geri yazar).
- veri-kalite: --ham ile ZORUNLU kimlik kontrolü (0 uyumsuz beklenir);
  --onceki karşılaştırmasında onarılanlar ayrı doğrulanır.
- docs/veri/yillik-guncelleme.md: kimlik kilidi adımı.

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

327 lines
14 KiB
TypeScript
Raw Permalink 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.
*
* 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<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;
// id → devir yılı: bu yıldan önceki kolonlar o satırda BİLEREK boş.
const devirYillari = new Map<string, number>();
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<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) {
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<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;
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<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(" ; ")}`);
}
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<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);