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

@@ -21,11 +21,23 @@
* 4. DB yazımı TEK transaction; önceki yılların kolon sağlaması transaction
* içinde karşılaştırılır, fark varsa geri alınır.
*
* 5. KİMLİK KİLİDİ: mevcut satırın puan türü / önlisans bayrağı / baz ad
* slug'ı API kaydıyla uyuşmuyorsa kılavuz kodu BAŞKA programa devredilmiştir
* (bkz. kimlik.ts). Liste basılır ve HİÇBİR ŞEY yazılmadan durulur;
* liste gözle incelendikten sonra `--kimlik-onar` ile yeniden koşulur.
*
* Yazdıkları: sira/puan/kontenjan/yerlesen<yil>, son_kilavuz_yili, eski_kod.
* Mevcut satırların ÖNCEKİ yıl kolonlarına dokunmaz (onları yalnız NULL ise
* gecmis-doldur.ts doldurur). Yeni program eklerken `…1/…2/…3` alanlarından
* önceki üç yılı da yazar. API "veri yok" için 0 döndürür → NULL.
*
* `--kimlik-onar`: kimliği kaymış satırlarda isim/tur/onlisans/fakulte/sure
* API'den yazılır; devirden ÖNCEKİ yılların tüm kolonları NULL'lanır (o geçmiş
* başka programa ait), eski kimlik + geçmişi `programs_arsiv`'e kopyalanır.
* `--isim-esitle`: kimliği AYNI olup yalnız adı farklı satırlarda (API burs/dil
* eki veriyor: "Tıp (İngilizce) (Burslu)") isim API'den yazılır. Varsayılan
* KAPALI: isim, sihirbaz kategorilerini ve bölüm sayfası başlığını besler.
*
* Kaynağa nazik: sayfa başı 500 kayıt, istekler arası 0,5 sn, tek bağlantı.
* `--ham <dosya>` verilirse API'ye HİÇ gidilmez; daha önce kaydedilmiş ham
* dosyadan aynı işlem tekrarlanır (kopyada deneme → gerçek DB'ye uygulama).
@@ -40,9 +52,20 @@ import readline from "node:readline";
import zlib from "node:zlib";
import { ILK_YIL } from "../src/lib/veri-yillari";
import { efektifIndeksiKur } from "./db-index";
import {
apiOnlisans,
devirYiliBul,
kimlikFarklari,
kimlikOnar,
netTurleriGetir,
type DbKimlik,
type KimlikFarki,
type OnarimKaydi,
} from "./kimlik";
import { uniAdiNormalize, uniturOnar } from "./unitur-onar";
import {
argOku,
bayrakVar,
calistir,
dbAc,
dbArgumani,
@@ -371,7 +394,13 @@ async function apidenCek(
return { toplam: first.totalElements, kayitlar, hamDosya };
}
function oncekiYilSaglamasi(db: Db, yil: number, azamiRowid: number): string {
/** Önceki yıl kolonlarının sağlaması; `haricIdler` (kimliği onarılanlar) dışarıda. */
function oncekiYilSaglamasi(
db: Db,
yil: number,
azamiRowid: number,
haricIdler: string[]
): string {
const kolonlar: string[] = [];
for (let y = ILK_YIL; y < yil; y++) {
for (const olcu of ["sira", "puan", "kontenjan", "yerlesen"]) {
@@ -379,9 +408,12 @@ function oncekiYilSaglamasi(db: Db, yil: number, azamiRowid: number): string {
}
}
const r = db
.prepare(`SELECT COUNT(*), ${kolonlar.join(", ")} FROM programs WHERE rowid <= ?`)
.prepare(
`SELECT COUNT(*), ${kolonlar.join(", ")} FROM programs
WHERE rowid <= ? AND id NOT IN (SELECT value FROM json_each(?))`
)
.raw()
.get(azamiRowid) as number[];
.get(azamiRowid, JSON.stringify(haricIdler)) as number[];
return r.join("|");
}
@@ -497,15 +529,124 @@ async function main() {
return mevcut || api;
};
// ── Kimlik kilidi: kod aynı, program aynı mı? ──────────────────────────
const kimlikOnarIste = bayrakVar("kimlik-onar");
const isimEsitleIste = bayrakVar("isim-esitle");
const kimlikGetir = db.prepare("SELECT * FROM programs WHERE id = ?");
type Uyumsuz = {
id: string;
r: KilavuzKaydi;
satir: DbKimlik & Record<string, unknown>;
farklar: KimlikFarki[];
apiTur: string | undefined;
devirYili: number;
};
const uyumsuzlar: Uyumsuz[] = [];
const ekFarkliIdler: string[] = [];
for (const [id, r] of benzersiz) {
const satir = kimlikGetir.get(id) as Uyumsuz["satir"] | undefined;
if (!satir) continue;
const apiTur = TUR_MAP[r.puanTuru?.trim() ?? ""];
const farklar = kimlikFarklari(satir, r, apiTur);
if (farklar.length) {
uyumsuzlar.push({
id,
r,
satir,
farklar,
apiTur,
devirYili: devirYiliBul(
satir,
{
gecmis: gecmisAlanlari(r),
netTurleri: netTurleriGetir(db, id, TUR_MAP),
eskiTur: satir.tur,
yeniTur: apiTur ?? satir.tur,
farklar,
},
yil
),
});
} else if (satir.isim !== r.birimAdi) {
ekFarkliIdler.push(id);
}
}
if (uyumsuzlar.length) {
console.log(`\nKİMLİĞİ KAYMIŞ ${uyumsuzlar.length} satır (kod aynı, program farklı):`);
for (const u of uyumsuzlar) {
const gecmis = [];
for (let y = ILK_YIL; y <= yil; y++) gecmis.push(u.satir[`sira${y}`] ?? "-");
const netOzet = [...netTurleriGetir(db, u.id, TUR_MAP)]
.sort((a, b) => a[0] - b[0])
.map(([y, t]) => `${y}=${t}`)
.join(" ");
console.log(
` ${u.id} [${u.farklar.join("+")}] ${u.satir.universite}\n` +
` DB : ${u.satir.isim} | ${u.satir.tur} | onlisans=${u.satir.onlisans} | ${u.satir.fakulte}\n` +
` API: ${u.r.birimAdi} | ${u.apiTur ?? `?(${u.r.puanTuru})`} | onlisans=${apiOnlisans(u.r)} | ${u.r.fymkAdi}\n` +
` sira ${ILK_YIL}…${yil}: ${gecmis.join(", ")} | net türleri: ${netOzet || "-"} → ` +
(u.devirYili === ILK_YIL
? "AD DEĞİŞİMİ (API penceresinin tamamı aynı program; geçmiş korunur)"
: `devir yılı ${u.devirYili} (öncesi silinir)`)
);
}
const tursuz = uyumsuzlar.filter((u) => !u.apiTur);
if (!kimlikOnarIste) {
dur(
`${uyumsuzlar.length} satırın kimliği API ile uyuşmuyor; DB'ye HİÇBİR ŞEY yazılmadı. ` +
"Listeyi incele; devirse aynı komutu --kimlik-onar ile yeniden koş " +
"(isim/tur/onlisans/fakulte/sure API'den yazılır, devir öncesi yıllar NULL'lanır, eski kimlik arşive kopyalanır)."
);
}
if (tursuz.length) {
dur(
`${tursuz.length} uyumsuz satırda API puan türü vermiyor (${tursuz.map((u) => u.id).join(", ")}); ` +
"tür UYDURULMAZ, onarım yapılamaz."
);
}
}
console.log(
`Kimlik: ${uyumsuzlar.length} uyumsuz; yalnız adı (burs/dil eki) farklı ${ekFarkliIdler.length} satır` +
(isimEsitleIste ? " → isim API'den yazılacak (--isim-esitle)" : " → isim yazılmıyor (--isim-esitle verilmedi)")
);
let updated = 0;
let inserted = 0;
const atlananlar: string[] = [];
const onarimlar: { id: string; universite: string; kayit: OnarimKaydi }[] = [];
let isimEsitlenen = 0;
db.transaction(() => {
const azamiRowid = (
db.prepare("SELECT COALESCE(MAX(rowid), 0) AS m FROM programs").get() as { m: number }
).m;
const once = oncekiYilSaglamasi(db, yil, azamiRowid);
const onarilanIdler = uyumsuzlar.map((u) => u.id);
const once = oncekiYilSaglamasi(db, yil, azamiRowid, onarilanIdler);
// Kimlik onarımı yıl yazımından ÖNCE: arşiv kopyası eski hâliyle alınır.
for (const u of uyumsuzlar) {
const universite = String(u.satir.universite);
const kayit = kimlikOnar(db, {
id: u.id,
yil,
devirYili: u.devirYili,
yeni: {
isim: u.r.birimAdi,
tur: u.apiTur as string,
onlisans: apiOnlisans(u.r),
fakulte: fakulteSec(universite, u.r.fymkAdi ?? null),
sure: u.r.ogrenimSuresi ?? null,
},
});
onarimlar.push({ id: u.id, universite, kayit });
}
if (isimEsitleIste) {
const isimYaz = db.prepare("UPDATE programs SET isim = ? WHERE id = ?");
for (const id of ekFarkliIdler) {
isimYaz.run((benzersiz.get(id) as KilavuzKaydi).birimAdi, id);
isimEsitlenen++;
}
}
for (const [id, r] of benzersiz) {
const tur = TUR_MAP[r.puanTuru?.trim() ?? ""];
@@ -549,10 +690,10 @@ async function main() {
}
}
const sonra = oncekiYilSaglamasi(db, yil, azamiRowid);
const sonra = oncekiYilSaglamasi(db, yil, azamiRowid, onarilanIdler);
if (once !== sonra) {
dur(
"Mevcut satırların önceki yıl kolonları değişti — bu betik onlara dokunmamalı. Geri alındı.\n" +
"Kimliği onarılanlar DIŞINDAKİ satırların önceki yıl kolonları değişti — bu betik onlara dokunmamalı. Geri alındı.\n" +
`önce : ${once}\nsonra: ${sonra}`
);
}
@@ -577,6 +718,20 @@ async function main() {
`Güncellenen: ${updated}, eklenen: ${inserted}, atlanan: ${kodsuz + atlananlar.length}`
);
for (const a of atlananlar) console.log(` atlandı: ${a}`);
if (isimEsitlenen) console.log(`İsmi API'den eşitlenen (yalnız ek farkı): ${isimEsitlenen}`);
if (onarimlar.length) {
console.log(`\nKİMLİĞİ ONARILAN ${onarimlar.length} satır:`);
console.log(" id | üniversite | eski ad [tür/önlisans] → yeni ad [tür/önlisans] | devir | NULL'lanan yıllar | arşiv | taşınan net yılları");
for (const o of onarimlar) {
const k = o.kayit;
console.log(
` ${o.id} | ${o.universite} | ${k.eski.isim} [${k.eski.tur}/${k.eski.onlisans}] → ` +
`${k.yeni.isim} [${k.yeni.tur}/${k.yeni.onlisans}] | ${k.devirYili} | ` +
`${k.silinenYillar.join(",") || "-"} | ${k.arsivId ?? "-"} | ${k.tasinanNetYillari.join(",") || "-"}`
);
}
metaYaz(db, `kimlik_onar_${yil}_satir`, onarimlar.length);
}
// API'nin universiteTuru döndürmediği kayıtlar devlet/vakıf filtrelerinden
// düşmesin: her tazelemeden sonra unitur onarımı koşulur (idempotent).