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>
This commit is contained in:
bilalgursen
2026-09-21 20:24:29 +03:00
parent 49683fa113
commit 2ff8fe584b
5 changed files with 560 additions and 13 deletions

View File

@@ -4,8 +4,13 @@
* UYARI'lar çıkış kodunu etkilemez; insan gözüyle bakılır.
*
* Kullanım:
* pnpm veri-kalite --yil 2026 --db /tmp/deneme.db [--onceki data/yokatlas.db] [--arsivli]
* pnpm veri-kalite --yil 2026 --db /tmp/deneme.db [--onceki data/yokatlas.db] [--arsivli] [--ham <dosya>]
*
* --ham <dosya> Ham API çekimi (yoksa data/ham altındaki en yenisi). Verilirse
* KİMLİK kontrolü koşar: her DB satırının puan türü / önlisans /
* baz ad slug'ı aynı koddaki API kaydıyla uyuşmalı (0 beklenir;
* uyuşmayan = kılavuz kodu başka programa devredilmiş, satır eski
* adıyla yeni programın sırasını gösteriyor → refresh --kimlik-onar).
* --onceki <db> Güncelleme ÖNCESİ DB. Verilirse: iki DB'de de bulunan her
* programın önceki yıl kolonları (sira/puan/kontenjan/yerlesen)
* birebir aynı mı (dolu değer değişmiş/silinmiş olamaz).
@@ -14,6 +19,8 @@
import path from "node:path";
import { EFEKTIF_SIRA_SQL, ILK_YIL } from "../src/lib/veri-yillari";
import { EFEKTIF_INDEKS, efektifSorguPlani } from "./db-index";
import { kimlikFarklari, onarimKayitlari, yilKolonlari, type DbKimlik } from "./kimlik";
import { hamOku, sonHamDosya, TUR_MAP } from "./refresh";
import {
argOku,
bayrakVar,
@@ -164,6 +171,52 @@ async function main() {
);
bilgi("eski_kod paylaşımı (bölünme)", eskiKodCakisma > 0, `birden çok güncel programın işaret ettiği eski kod: ${eskiKodCakisma}`);
// 9b. Kimlik: kod aynı, program aynı mı? (ham çekim gerekir)
const hamArg = argOku("ham");
const hamDosya = hamArg
? path.resolve(process.cwd(), hamArg)
: sonHamDosya(yil, path.resolve(process.cwd(), "data", "ham"));
const onarimlar = onarimKayitlari(db);
if (hamDosya) {
const { kayitlar } = await hamOku(hamDosya, yil);
const getir = db.prepare("SELECT id, isim, tur, onlisans, fakulte, sure, universite FROM programs WHERE id = ?");
const uyumsuz: string[] = [];
let kiyaslanan = 0;
let ekFarki = 0;
for (const r of kayitlar) {
if (!r.kilavuzKodu) continue;
const satir = getir.get(String(r.kilavuzKodu)) as (DbKimlik & { universite: string }) | undefined;
if (!satir) continue;
kiyaslanan++;
const farklar = kimlikFarklari(satir, r, TUR_MAP[r.puanTuru?.trim() ?? ""]);
if (farklar.length) {
uyumsuz.push(`${satir.id}[${farklar.join("+")}] DB="${satir.isim}"/${satir.tur}/${satir.onlisans} API="${r.birimAdi}"/${r.puanTuru}/${r.birimTuruId === 47 ? 1 : 0} (${satir.universite})`);
} else if (satir.isim !== r.birimAdi) {
ekFarki++;
}
}
zorunlu(
"kimlik (tur/önlisans/baz ad = API)",
uyumsuz.length === 0,
`${kiyaslanan} satır kıyaslandı, uyumsuz: ${uyumsuz.length}${uyumsuz.length ? ` ör. ${uyumsuz.slice(0, 3).join(" ; ")}` : ""} [${path.basename(hamDosya)}]`
);
bilgi("isim eki (burs/dil) API'den farklı", false, `${ekFarki} satır — kimlik aynı, DB adı eksiz/farklı yazımlı (aynı üniversitede adaş varyantlar ayırt edilemez)`);
} else {
bilgi("kimlik", true, "ATLANDI: --ham verilmedi ve data/ham altında çekim yok; kod devri denetlenemedi");
}
if (onarimlar.size) {
// Onarılan satırda devir öncesi hiçbir yıl kolonu dolu kalmış olamaz.
const yk = yilKolonlari(db);
let sizinti = 0;
const satirGetir = db.prepare("SELECT * FROM programs WHERE id = ?");
for (const [id, k] of onarimlar) {
const s = satirGetir.get(id) as Record<string, unknown> | undefined;
if (!s) continue;
if (yk.some((c) => c.yil < k.devirYili && s[c.kolon] != null)) sizinti++;
}
zorunlu("kimliği onarılanlar: devir öncesi boş", sizinti === 0, `${onarimlar.size} onarılmış satırın ${sizinti} tanesinde devir öncesi yıl yeniden dolmuş`);
}
// 10. netler
if (tabloVar(db, "netler")) {
const arsivVar = tabloVar(db, "programs_arsiv");
@@ -202,11 +255,29 @@ async function main() {
const kaybolan = sayi(`SELECT COUNT(*) AS n FROM eski.programs e WHERE e.id NOT IN (SELECT id FROM ${simdi})`);
zorunlu("önceki DB: satır kaybı", kaybolan === 0, `önceki DB'de olup şimdi ne programs ne arşivde olan: ${kaybolan}`);
// Dolu değer değişmiş ya da NULL'a dönmüş olamaz; NULL → dolu (gecmis-doldur) serbest.
// Kimliği onarılan satırlarda devir öncesi yıllar BİLEREK NULL'landı; onlar
// ayrı sayılır: devir yılı ve sonrası yine birebir korunmuş olmalı.
const onarilanJson = JSON.stringify([...onarimlar.keys()]);
const degisen = sayi(
`SELECT COUNT(*) AS n FROM eski.programs e JOIN ${simdi} s ON s.id = e.id
WHERE ${ortak.map((k) => `(e.${k} IS NOT NULL AND e.${k} IS NOT s.${k})`).join(" OR ")}`
WHERE e.id NOT IN (SELECT value FROM json_each(?))
AND (${ortak.map((k) => `(e.${k} IS NOT NULL AND e.${k} IS NOT s.${k})`).join(" OR ")})`,
onarilanJson
);
zorunlu("önceki DB: geçmiş yıllar korunmuş", degisen === 0, `${ortak.length} kolonda dolu değeri değişen/silinen satır: ${degisen}`);
zorunlu("önceki DB: geçmiş yıllar korunmuş", degisen === 0, `${ortak.length} kolonda dolu değeri değişen/silinen satır: ${degisen} (kimliği onarılan ${onarimlar.size} satır hariç)`);
if (onarimlar.size) {
let bozulan = 0;
const eskiGetir = db.prepare("SELECT * FROM eski.programs WHERE id = ?");
const yeniGetir = db.prepare("SELECT * FROM main.programs WHERE id = ?");
for (const [id, k] of onarimlar) {
const e = eskiGetir.get(id) as Record<string, unknown> | undefined;
const s = yeniGetir.get(id) as Record<string, unknown> | undefined;
if (!e || !s) continue;
const korunmali = ortak.filter((kolon) => Number(kolon.slice(-4)) >= k.devirYili);
if (korunmali.some((kolon) => e[kolon] != null && e[kolon] !== s[kolon])) bozulan++;
}
zorunlu("önceki DB: onarılanlarda devir sonrası korunmuş", bozulan === 0, `devir yılı ve sonrasında değeri değişen onarılmış satır: ${bozulan}/${onarimlar.size}`);
}
const toplamlar = (tablo: string) =>
(db
.prepare(