merge: fix/2026-yayin-sonrasi — tadımlık canlı tabandan, liste riskleri yenilenir, 30 program kimliği onarıldı
All checks were successful
Deploy / deploy (push) Successful in 7m11s
All checks were successful
Deploy / deploy (push) Successful in 7m11s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -19,12 +19,17 @@
|
||||
* 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,
|
||||
@@ -95,6 +100,16 @@ async function main() {
|
||||
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);
|
||||
@@ -109,6 +124,7 @@ async function main() {
|
||||
/** 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;
|
||||
@@ -152,6 +168,7 @@ async function main() {
|
||||
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;
|
||||
@@ -279,6 +296,13 @@ async function main() {
|
||||
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()) {
|
||||
|
||||
281
scripts/kimlik.ts
Normal file
281
scripts/kimlik.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
/**
|
||||
* Program KİMLİĞİ tutarlılığı: kılavuz kodu (programs.id) yıllar içinde BAŞKA
|
||||
* bir programa devredilebiliyor (ör. 209210292: 2024'te "Diş Hekimliği, SAY
|
||||
* lisans" → 2025'ten beri "İlk ve Acil Yardım, TYT önlisans"). refresh yalnız
|
||||
* koda bakıp yıl kolonlarını yazarsa satır ESKİ adıyla YENİ programın sırasını
|
||||
* gösterir. Bu modül refresh / veri-kalite / gecmis-doldur'un ortak kurallarıdır.
|
||||
*
|
||||
* Kimlik = puan türü + önlisans bayrağı + baz ad slug'ı
|
||||
* (`turkishSlugify(bolumBazAdi(isim))`; burs/dil ekleri kimlik DEĞİLDİR —
|
||||
* DB'deki eski satırlar eksiz, API ekli yazıyor).
|
||||
*
|
||||
* DEVİR YILI (kanıt zinciri; tahmin yok): SON yıl kesin olarak yeni programındır
|
||||
* (API kaydı o kılavuzdan). Geriye doğru her yıl için API'nin `…1/…2/…3` sıra/
|
||||
* puanı DB'deki değerle EŞLEŞİYORSA o yıl da yeni programındır; eşleşmeyen ya
|
||||
* da API'nin sıra/puanla sahiplenmediği İLK yılda zincir kopar — o yıl ve
|
||||
* öncesi eski programa aittir. Zincir ŞART, çünkü API geçmişi kod bazlıdır ve
|
||||
* kirlidir: `gk2` hep eski programın kontenjanıdır, `basariSirasi3` bazı
|
||||
* kayıtlarda eski programın sırasıdır (ör. 205590251: TYT Mekatronik'e 2023 için
|
||||
* DİL sırası 108.099). Sıra eşitliği yerine puan eşitliği de kabul edilir:
|
||||
* 2024 sırası iki kaynakta farklı tanımlı (bkz. gecmis-doldur), puan aynıdır.
|
||||
* İkinci bağımsız kaynak `netler.puanTuru`'dur (YÖK Atlas net API'si, yıl bazlı).
|
||||
*/
|
||||
import { ILK_YIL } from "../src/lib/veri-yillari";
|
||||
import { bolumBazAdi, turkishSlugify } from "../src/lib/slug";
|
||||
import { kolonAdlari, metaTablosunuGarantiEt, tabloVar, type Db } from "./yokatlas-goc";
|
||||
|
||||
/** refresh.ts ile döngüsel import olmasın diye kimliğin gerektirdiği alanlar. */
|
||||
export type KimlikKaydi = {
|
||||
kilavuzKodu: number;
|
||||
birimAdi: string;
|
||||
fymkAdi?: string | null;
|
||||
puanTuru: string;
|
||||
ogrenimSuresi?: number | null;
|
||||
birimTuruId: number;
|
||||
};
|
||||
|
||||
export type GecmisAlani = { fark: number; sira: number | null; puan: number | null };
|
||||
|
||||
export type DbKimlik = {
|
||||
id: string;
|
||||
isim: string;
|
||||
tur: string;
|
||||
onlisans: number;
|
||||
fakulte: string | null;
|
||||
sure: number | null;
|
||||
};
|
||||
|
||||
export type KimlikFarki = "tur" | "onlisans" | "ad";
|
||||
|
||||
const PUAN_TOLERANS = 0.01;
|
||||
const META_ONEK = "kimlik_onar:";
|
||||
|
||||
export const bazAdSlug = (isim: string): string => turkishSlugify(bolumBazAdi(isim));
|
||||
|
||||
/** ÖSYM birim türü 47 = önlisans (refresh INSERT dalıyla aynı kural). */
|
||||
export const apiOnlisans = (r: KimlikKaydi): number => (r.birimTuruId === 47 ? 1 : 0);
|
||||
|
||||
/**
|
||||
* DB satırı ile API kaydının kimlik farkları. `apiTur` çözülemediyse (kaynak
|
||||
* puan türünü vermiyor) tür KIYASLANMAZ — bilinmeyen, fark sayılmaz.
|
||||
*/
|
||||
export function kimlikFarklari(
|
||||
satir: DbKimlik,
|
||||
r: KimlikKaydi,
|
||||
apiTur: string | undefined
|
||||
): KimlikFarki[] {
|
||||
const farklar: KimlikFarki[] = [];
|
||||
if (apiTur && apiTur !== satir.tur) farklar.push("tur");
|
||||
if (apiOnlisans(r) !== satir.onlisans) farklar.push("onlisans");
|
||||
if (bazAdSlug(satir.isim) !== bazAdSlug(r.birimAdi)) farklar.push("ad");
|
||||
return farklar;
|
||||
}
|
||||
|
||||
export type DevirKaniti = {
|
||||
/** API `…1/…2/…3` alanları. */
|
||||
gecmis: GecmisAlani[];
|
||||
/** `netler.puanTuru` (yıl → DB tür sözlüğüne çevrilmiş); bağımsız ikinci kaynak. */
|
||||
netTurleri?: Map<number, string>;
|
||||
eskiTur: string;
|
||||
yeniTur: string;
|
||||
farklar: KimlikFarki[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Yeni programa ait olduğu KANITLANAN ilk yıl. `yil` (son yıl) her zaman
|
||||
* yenidir; geriye doğru zincir ilk kanıtsız yılda kopar (bkz. dosya başı).
|
||||
*
|
||||
* Bir yılın kanıtı: API sıra/puanı DB ile eşleşir. DB'de o yılın sıra/puanı HİÇ
|
||||
* yoksa (dolmayan program; API'nin doğrulayacağı/sahiplenmeyeceği taban yok) ve
|
||||
* tür değiştiyse, o yılın `netler` kaydının YENİ türde olması da yeter — böylece
|
||||
* yalnız kontenjan/yerleşen/net korunur, yanlış etiketlenebilecek sıra kalmaz.
|
||||
* DB'de sıra VARKEN API o yılı boş veriyorsa net türü yetmez: aynı türde başka
|
||||
* bir program olabilir (dolu programda API'nin 2024'ü boş verme oranı uyumlu
|
||||
* satırlarda %0,3; kimliği kaymışlarda 22/23 — API o yılı bilerek sahiplenmiyor).
|
||||
* Karşı kanıt: tür değiştiyse ve o yılın neti ESKİ türdeyse yıl kesin eskidir.
|
||||
*
|
||||
* AD DEĞİŞİMİ: fark yalnız addaysa ve zincir API penceresinin TAMAMINDA (3 yıl)
|
||||
* sağlamsa, API eski adın geçerli olduğu yılları da bu programa sayıyor demektir
|
||||
* → program aynı, adı değişmiş; geçmiş korunur (ILK_YIL döner).
|
||||
*/
|
||||
export function devirYiliBul(
|
||||
satir: Record<string, unknown>,
|
||||
kanit: DevirKaniti,
|
||||
yil: number
|
||||
): number {
|
||||
const turDegisti = kanit.eskiTur !== kanit.yeniTur;
|
||||
const gecmis = [...kanit.gecmis].sort((a, b) => a.fark - b.fark);
|
||||
let devir = yil;
|
||||
for (const g of gecmis) {
|
||||
const y = yil - g.fark;
|
||||
if (y < ILK_YIL || y !== devir - 1) break;
|
||||
const netTur = kanit.netTurleri?.get(y);
|
||||
if (turDegisti && netTur === kanit.eskiTur) break;
|
||||
const dbSira = satir[`sira${y}`] as number | null;
|
||||
const dbPuan = satir[`puan${y}`] as number | null;
|
||||
const siraAyni = g.sira != null && dbSira != null && g.sira === dbSira;
|
||||
const puanAyni =
|
||||
g.puan != null && dbPuan != null && Math.abs(g.puan - dbPuan) <= PUAN_TOLERANS;
|
||||
const netYeni =
|
||||
dbSira == null && dbPuan == null && turDegisti && netTur === kanit.yeniTur;
|
||||
if (!siraAyni && !puanAyni && !netYeni) break;
|
||||
devir = y;
|
||||
}
|
||||
const pencereSonu = Math.max(ILK_YIL, yil - gecmis.length);
|
||||
const yalnizAd = kanit.farklar.length === 1 && kanit.farklar[0] === "ad";
|
||||
if (yalnizAd && gecmis.length > 0 && devir === pencereSonu) return ILK_YIL;
|
||||
return devir;
|
||||
}
|
||||
|
||||
/** `netler.puanTuru` yıl haritası (tablo yoksa boş). Değerler API kısa kodudur (SAY, TYT…). */
|
||||
export function netTurleriGetir(db: Db, id: string, turSozlugu: Record<string, string>): Map<number, string> {
|
||||
const sonuc = new Map<number, string>();
|
||||
if (!tabloVar(db, "netler")) return sonuc;
|
||||
const satirlar = db
|
||||
.prepare("SELECT yil, puanTuru FROM netler WHERE program_id = ? AND puanTuru IS NOT NULL")
|
||||
.all(id) as { yil: number; puanTuru: string }[];
|
||||
for (const n of satirlar) {
|
||||
const tur = turSozlugu[n.puanTuru.trim()];
|
||||
if (tur) sonuc.set(n.yil, tur);
|
||||
}
|
||||
return sonuc;
|
||||
}
|
||||
|
||||
/** programs'ın yıla bağlı TÜM kolonları (sira2024, gk2025, obkY2025…) → yıl. */
|
||||
export function yilKolonlari(db: Db, tablo = "programs"): { kolon: string; yil: number }[] {
|
||||
const sonuc: { kolon: string; yil: number }[] = [];
|
||||
for (const kolon of kolonAdlari(db, tablo)) {
|
||||
const m = /^[A-Za-z0-9]*?[A-Za-z](\d{4})$/.exec(kolon);
|
||||
if (!m) continue;
|
||||
const y = Number(m[1]);
|
||||
if (y >= ILK_YIL && y <= 2100) sonuc.push({ kolon, yil: y });
|
||||
}
|
||||
return sonuc;
|
||||
}
|
||||
|
||||
export type OnarimKaydi = {
|
||||
devirYili: number;
|
||||
yil: number;
|
||||
eski: { isim: string; tur: string; onlisans: number; fakulte: string | null; sure: number | null };
|
||||
yeni: { isim: string; tur: string; onlisans: number; fakulte: string | null; sure: number | null };
|
||||
silinenYillar: number[];
|
||||
arsivId: string | null;
|
||||
tasinanNetYillari: number[];
|
||||
};
|
||||
|
||||
export type OnarimGirdisi = {
|
||||
id: string;
|
||||
yil: number;
|
||||
devirYili: number;
|
||||
yeni: OnarimKaydi["yeni"];
|
||||
};
|
||||
|
||||
/**
|
||||
* Tek satırın kimliğini onarır (çağıran transaction içinde olmalı):
|
||||
* 1. Devirden önce verisi/neti varsa eski kimliğiyle `programs_arsiv`'e kopya:
|
||||
* id `<kod>~<son veri yılı>` (kod güncel satırda yaşamaya devam ettiği için
|
||||
* arşivde aynı id kullanılamaz; ileride eski-arsivle ile PK çakışırdı).
|
||||
* Kopyada devir yılı ve sonrası NULL (o yıllar yeni programın).
|
||||
* 2. Devirden önceki `netler` satırları arşiv id'sine taşınır (yeni programın
|
||||
* sayfasında eski programın netleri görünmesin; veri silinmez).
|
||||
* 3. Güncel satırda devirden önceki TÜM yıl kolonları NULL; isim/tur/onlisans/
|
||||
* fakulte/sure API'den yazılır.
|
||||
* 4. `veri_meta`'ya `kimlik_onar:<id>` kaydı — gecmis-doldur bu yılların
|
||||
* API'nin kirli `…N` alanlarından yeniden dolmasını bununla engeller.
|
||||
*/
|
||||
export function kimlikOnar(db: Db, g: OnarimGirdisi): OnarimKaydi {
|
||||
const satir = db.prepare("SELECT * FROM programs WHERE id = ?").get(g.id) as
|
||||
| Record<string, string | number | null>
|
||||
| undefined;
|
||||
if (!satir) throw new Error(`kimlikOnar: satır yok ${g.id}`);
|
||||
const kolonlar = yilKolonlari(db);
|
||||
const onceki = kolonlar.filter((k) => k.yil < g.devirYili);
|
||||
const sonraki = kolonlar.filter((k) => k.yil >= g.devirYili);
|
||||
const silinenYillar = [
|
||||
...new Set(onceki.filter((k) => satir[k.kolon] != null).map((k) => k.yil)),
|
||||
].sort();
|
||||
const netYillari = tabloVar(db, "netler")
|
||||
? (
|
||||
db
|
||||
.prepare("SELECT yil FROM netler WHERE program_id = ? AND yil < ? ORDER BY yil")
|
||||
.all(g.id, g.devirYili) as { yil: number }[]
|
||||
).map((n) => n.yil)
|
||||
: [];
|
||||
|
||||
let arsivId: string | null = null;
|
||||
const sonVeriYili = Math.max(-1, ...silinenYillar, ...netYillari);
|
||||
if (sonVeriYili > 0 && tabloVar(db, "programs_arsiv")) {
|
||||
arsivId = `${g.id}~${sonVeriYili}`;
|
||||
const arsivKolonlari = new Set(kolonAdlari(db, "programs_arsiv"));
|
||||
const tasinan = Object.keys(satir).filter((k) => arsivKolonlari.has(k));
|
||||
const degerler = tasinan.map((k) => {
|
||||
if (k === "id") return arsivId;
|
||||
// Eski programın bu kodla verisinin bulunduğu son yıl (olgu, tahmin değil)
|
||||
if (k === "son_kilavuz_yili") return sonVeriYili;
|
||||
if (sonraki.some((s) => s.kolon === k)) return null;
|
||||
return satir[k];
|
||||
});
|
||||
db.prepare(
|
||||
`INSERT INTO programs_arsiv (${tasinan.join(", ")}) VALUES (${tasinan.map(() => "?").join(", ")})`
|
||||
).run(...degerler);
|
||||
if (netYillari.length) {
|
||||
db.prepare("UPDATE netler SET program_id = ? WHERE program_id = ? AND yil < ?").run(
|
||||
arsivId,
|
||||
g.id,
|
||||
g.devirYili
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const atamalar = [
|
||||
...onceki.map((k) => `${k.kolon} = NULL`),
|
||||
"isim = ?",
|
||||
"tur = ?",
|
||||
"onlisans = ?",
|
||||
"fakulte = ?",
|
||||
"sure = ?",
|
||||
];
|
||||
db.prepare(`UPDATE programs SET ${atamalar.join(", ")} WHERE id = ?`).run(
|
||||
g.yeni.isim,
|
||||
g.yeni.tur,
|
||||
g.yeni.onlisans,
|
||||
g.yeni.fakulte,
|
||||
g.yeni.sure,
|
||||
g.id
|
||||
);
|
||||
|
||||
const kayit: OnarimKaydi = {
|
||||
devirYili: g.devirYili,
|
||||
yil: g.yil,
|
||||
eski: {
|
||||
isim: String(satir.isim),
|
||||
tur: String(satir.tur),
|
||||
onlisans: Number(satir.onlisans),
|
||||
fakulte: satir.fakulte as string | null,
|
||||
sure: satir.sure as number | null,
|
||||
},
|
||||
yeni: g.yeni,
|
||||
silinenYillar,
|
||||
arsivId,
|
||||
tasinanNetYillari: arsivId ? netYillari : [],
|
||||
};
|
||||
metaTablosunuGarantiEt(db);
|
||||
db.prepare(
|
||||
"INSERT INTO veri_meta (anahtar, deger) VALUES (?, ?) " +
|
||||
"ON CONFLICT(anahtar) DO UPDATE SET deger = excluded.deger"
|
||||
).run(`${META_ONEK}${g.id}`, JSON.stringify(kayit));
|
||||
return kayit;
|
||||
}
|
||||
|
||||
/** Daha önce onarılmış satırlar: id → kayıt (gecmis-doldur ve veri-kalite okur). */
|
||||
export function onarimKayitlari(db: Db): Map<string, OnarimKaydi> {
|
||||
const sonuc = new Map<string, OnarimKaydi>();
|
||||
if (!tabloVar(db, "veri_meta")) return sonuc;
|
||||
const satirlar = db
|
||||
.prepare("SELECT anahtar, deger FROM veri_meta WHERE anahtar LIKE ?")
|
||||
.all(`${META_ONEK}%`) as { anahtar: string; deger: string }[];
|
||||
for (const s of satirlar) {
|
||||
sonuc.set(s.anahtar.slice(META_ONEK.length), JSON.parse(s.deger) as OnarimKaydi);
|
||||
}
|
||||
return sonuc;
|
||||
}
|
||||
@@ -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).
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user