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>
327 lines
14 KiB
TypeScript
327 lines
14 KiB
TypeScript
/**
|
||
* 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);
|