- yokatlas-goc: idempotent şema göçü (yıl kolonları, son_kilavuz_yili, eski_kod, programs_arsiv, veri_meta) + betiklerin ortak yardımcıları - refresh: --yil/--db zorunlu, YIL KİLİDİ (…1 = DB sira<yil-1> ≥%95 ve eksiz ≠), yıl asla r.yil'den türetilmez; ham veri data/ham/*.jsonl.gz, --ham ile API'siz tekrar; benzersiz kod = totalElements; tek transaction + önceki yıl sağlaması; yeni programda …1/2/3 → üç önceki yıl; efektif indeks EFEKTIF_SIRA_SQL ile - gecmis-doldur: yalnız NULL alan; API …1/2/3 → eski_kod → kanıtlı 1:1 ad eşleşmesi - eski-arsivle: güncel kılavuzda olmayanlar → programs_arsiv, netler yeni koda; --kuru - detay: netler bağımsız + yıl sağlaması; kırılım yalnız --kirilim, alan yoksa durur, mevcut değerin üstüne NULL yazılmaz; `yil - 1` varsayımı kalktı - veri-kalite: salt okunur kalite kapısı, zorunlu kontrol kalırsa çıkış kodu 1 - docs/veri/yillik-guncelleme.md: adım adım runbook + geri alma DB commit'lenmedi (arşivleme kararı [Bilal]). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
608 lines
22 KiB
TypeScript
608 lines
22 KiB
TypeScript
/**
|
||
* Canlı YÖK Atlas API'sinden SON yerleştirme yılının verisini çekip DB'ye işler.
|
||
*
|
||
* Endpoint: POST https://yokatlas.yok.gov.tr/api/tercih-kilavuz/search
|
||
*
|
||
* API'nin yıl düzeni (Eylül 2026'da doğrulandı): EKSİZ alanlar (basariSirasi,
|
||
* minPuan, kontenjan, gkY…) en son yerleştirme yılıdır; `…1` bir önceki,
|
||
* `…2` iki önceki, `…3` üç önceki yıldır. Kayıttaki `yil` alanı GÜVENİLMEZ
|
||
* (kılavuz yılı / veri yılı anlamı yıl içinde değişiyor) — bu betik yılı
|
||
* HİÇBİR ZAMAN oradan türetmez. Yıl `--yil` ile verilir ve YIL KİLİDİ ile
|
||
* DB'ye karşı doğrulanır.
|
||
*
|
||
* Güvenlik sırası:
|
||
* 1. `--yil` = SON_YIL, `--db` zorunlu (varsayılan yok).
|
||
* 2. YIL KİLİDİ: ilk sayfadaki `basariSirasi1` değerleri DB'deki
|
||
* sira<yil-1> ile ≥%95 eşleşmeli VE eksiz `basariSirasi` ondan farklı
|
||
* olmalı. Değilse API henüz yeni yıla dönmemiştir (ya da bir yıl daha
|
||
* ilerlemiştir) → HİÇBİR ŞEY yazılmadan durur.
|
||
* 3. Tüm sayfalar önce belleğe + ham dosyaya alınır; benzersiz kod sayısı =
|
||
* `totalElements` değilse (sayfalama kayması) durur.
|
||
* 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.
|
||
*
|
||
* 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.
|
||
*
|
||
* 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).
|
||
*
|
||
* Kullanım:
|
||
* pnpm refresh --yil 2026 --db /tmp/deneme.db
|
||
* pnpm refresh --yil 2026 --db data/yokatlas.db --ham data/ham/2026-2026-09-21.jsonl.gz
|
||
*/
|
||
import fs from "node:fs";
|
||
import path from "node:path";
|
||
import readline from "node:readline";
|
||
import zlib from "node:zlib";
|
||
import { ILK_YIL } from "../src/lib/veri-yillari";
|
||
import { efektifIndeksiKur } from "./db-index";
|
||
import { uniAdiNormalize, uniturOnar } from "./unitur-onar";
|
||
import {
|
||
argOku,
|
||
calistir,
|
||
dbAc,
|
||
dbArgumani,
|
||
dur,
|
||
metaYaz,
|
||
yilArgumani,
|
||
yilKolonlariniGarantiEt,
|
||
yokatlasGoc,
|
||
type Db,
|
||
} from "./yokatlas-goc";
|
||
|
||
const API = "https://yokatlas.yok.gov.tr/api/tercih-kilavuz/search";
|
||
const PAGE_SIZE = 500;
|
||
const DELAY_MS = 500;
|
||
const KILIT_ESIK = 0.95;
|
||
const KILIT_ASGARI_ORNEK = 50;
|
||
|
||
export const TUR_MAP: Record<string, string> = {
|
||
SAY: "SAYISAL",
|
||
EA: "EŞİT AĞIRLIK",
|
||
SÖZ: "SÖZEL",
|
||
DİL: "DİL",
|
||
TYT: "TYT",
|
||
};
|
||
|
||
/** API kaydından betiklerin kullandığı alanlar (ham dosyada kaydın tamamı durur). */
|
||
export type KilavuzKaydi = {
|
||
kilavuzKodu: number;
|
||
eskiKilavuzKodu?: number | null;
|
||
birimAdi: string;
|
||
universiteAdi: string;
|
||
universiteTuru?: string | null;
|
||
ilAdi?: string | null;
|
||
fymkAdi?: string | null;
|
||
puanTuru: string;
|
||
ogrenimSuresi?: number | null;
|
||
birimTuruId: number;
|
||
basariSirasi?: number | string | null;
|
||
minPuan?: number | string | null;
|
||
kontenjan?: number | null;
|
||
gkY?: number | null;
|
||
sgyY?: number | null;
|
||
dprmY?: number | null;
|
||
obkY?: number | null;
|
||
basariSirasi1?: number | string | null;
|
||
basariSirasi2?: number | string | null;
|
||
basariSirasi3?: number | string | null;
|
||
minPuan1?: number | string | null;
|
||
minPuan2?: number | string | null;
|
||
minPuan3?: number | string | null;
|
||
gk1?: number | null;
|
||
gk2?: number | null;
|
||
gk3?: number | null;
|
||
};
|
||
|
||
const TUTULAN_ALANLAR: (keyof KilavuzKaydi)[] = [
|
||
"kilavuzKodu",
|
||
"eskiKilavuzKodu",
|
||
"birimAdi",
|
||
"universiteAdi",
|
||
"universiteTuru",
|
||
"ilAdi",
|
||
"fymkAdi",
|
||
"puanTuru",
|
||
"ogrenimSuresi",
|
||
"birimTuruId",
|
||
"basariSirasi",
|
||
"minPuan",
|
||
"kontenjan",
|
||
"gkY",
|
||
"sgyY",
|
||
"dprmY",
|
||
"obkY",
|
||
"basariSirasi1",
|
||
"basariSirasi2",
|
||
"basariSirasi3",
|
||
"minPuan1",
|
||
"minPuan2",
|
||
"minPuan3",
|
||
"gk1",
|
||
"gk2",
|
||
"gk3",
|
||
];
|
||
|
||
function inceKayit(ham: Record<string, unknown>): KilavuzKaydi {
|
||
const ince: Record<string, unknown> = {};
|
||
for (const alan of TUTULAN_ALANLAR) {
|
||
if (ham[alan] !== undefined) ince[alan] = ham[alan];
|
||
}
|
||
return ince as KilavuzKaydi;
|
||
}
|
||
|
||
type ApiPage = {
|
||
content: Record<string, unknown>[];
|
||
totalPages: number;
|
||
totalElements: number;
|
||
};
|
||
|
||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
||
|
||
async function fetchPage(page: number): Promise<ApiPage> {
|
||
const res = await fetch(API, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
filters: {
|
||
puanTuru: null,
|
||
universiteId: [],
|
||
birimGrupId: [],
|
||
ilKodu: [],
|
||
birimTuruId: null,
|
||
universiteTuru: null,
|
||
bursOraniId: null,
|
||
ogrenimTuruId: null,
|
||
kilavuzKodu: null,
|
||
minBasariSirasi: null,
|
||
maxBasariSirasi: null,
|
||
},
|
||
page,
|
||
size: PAGE_SIZE,
|
||
sortBy: "kilavuzKodu",
|
||
direction: "ASC",
|
||
}),
|
||
});
|
||
if (!res.ok) {
|
||
throw new Error(`API ${res.status} (sayfa ${page})`);
|
||
}
|
||
return (await res.json()) as ApiPage;
|
||
}
|
||
|
||
/** Pozitif tam sayı; 0 / boş / sayı değil → NULL (API "veri yok"u 0 ile verir). */
|
||
export function intOrNull(v: unknown): number | null {
|
||
const n = typeof v === "string" ? Number.parseFloat(v) : (v as number);
|
||
return typeof n === "number" && Number.isFinite(n) && n > 0
|
||
? Math.round(n)
|
||
: null;
|
||
}
|
||
|
||
export function realOrNull(v: unknown): number | null {
|
||
const n = typeof v === "string" ? Number.parseFloat(v) : (v as number);
|
||
return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : null;
|
||
}
|
||
|
||
function yerlesen(r: KilavuzKaydi): number | null {
|
||
const parts = [r.gkY, r.sgyY, r.dprmY, r.obkY].filter(
|
||
(x): x is number => typeof x === "number"
|
||
);
|
||
return parts.length ? parts.reduce((a, b) => a + b, 0) : null;
|
||
}
|
||
|
||
/**
|
||
* API `eskiKilavuzKodu` iki anlamda geliyor: gerçek eski program kodu (9 hane)
|
||
* ya da yeni açılan programda üniversite öneki (4 hane, ör. 2031). Yalnız
|
||
* gerçek program kodu biçimindeki ve yeni koddan farklı olanı döndürür.
|
||
*/
|
||
export function eskiKod(r: KilavuzKaydi): string | null {
|
||
if (!r.eskiKilavuzKodu) return null;
|
||
const eski = String(r.eskiKilavuzKodu);
|
||
const yeni = String(r.kilavuzKodu);
|
||
return eski.length === yeni.length && eski !== yeni ? eski : null;
|
||
}
|
||
|
||
/** Yeni programda önceki üç yılın API karşılığı: [yıl farkı, sıra, puan, genel kontenjan]. */
|
||
export function gecmisAlanlari(
|
||
r: KilavuzKaydi
|
||
): { fark: number; sira: number | null; puan: number | null; kontenjan: number | null }[] {
|
||
return [
|
||
{ fark: 1, sira: intOrNull(r.basariSirasi1), puan: realOrNull(r.minPuan1), kontenjan: intOrNull(r.gk1) },
|
||
{ fark: 2, sira: intOrNull(r.basariSirasi2), puan: realOrNull(r.minPuan2), kontenjan: intOrNull(r.gk2) },
|
||
{ fark: 3, sira: intOrNull(r.basariSirasi3), puan: realOrNull(r.minPuan3), kontenjan: intOrNull(r.gk3) },
|
||
];
|
||
}
|
||
|
||
// ── Ham dosya ────────────────────────────────────────────────────────────
|
||
|
||
/** `--ham` verilmemişse o yılın en yeni ham dosyası (ad sırası = tarih sırası). */
|
||
export function sonHamDosya(yil: number, dizin: string): string | null {
|
||
if (!fs.existsSync(dizin)) return null;
|
||
const adaylar = fs
|
||
.readdirSync(dizin)
|
||
.filter((f) => f.startsWith(`${yil}-`) && f.endsWith(".jsonl.gz"))
|
||
.sort();
|
||
return adaylar.length ? path.join(dizin, adaylar[adaylar.length - 1]) : null;
|
||
}
|
||
|
||
export type HamOkuma = { toplam: number; kayitlar: KilavuzKaydi[] };
|
||
|
||
/** Ham .jsonl.gz → ince kayıtlar. İlk satır `_meta` (yıl doğrulanır). */
|
||
export async function hamOku(dosya: string, yil: number): Promise<HamOkuma> {
|
||
if (!fs.existsSync(dosya)) dur(`Ham dosya yok: ${dosya}`);
|
||
const rl = readline.createInterface({
|
||
input: fs.createReadStream(dosya).pipe(zlib.createGunzip()),
|
||
crlfDelay: Infinity,
|
||
});
|
||
let toplam = -1;
|
||
const kayitlar: KilavuzKaydi[] = [];
|
||
for await (const satir of rl) {
|
||
if (!satir) continue;
|
||
const obj = JSON.parse(satir) as Record<string, unknown>;
|
||
if (obj._meta) {
|
||
const meta = obj._meta as { yil: number; totalElements: number };
|
||
if (meta.yil !== yil) {
|
||
dur(`Ham dosya ${meta.yil} yılı için çekilmiş, --yil ${yil} verildi.`);
|
||
}
|
||
toplam = meta.totalElements;
|
||
continue;
|
||
}
|
||
kayitlar.push(inceKayit(obj));
|
||
}
|
||
if (toplam < 0) dur(`Ham dosyada _meta satırı yok: ${dosya}`);
|
||
return { toplam, kayitlar };
|
||
}
|
||
|
||
// ── Yıl kilidi ───────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* `basariSirasi1` = DB sira<yil-1> (≥%95) VE eksiz `basariSirasi` ≠ DB
|
||
* sira<yil-1>. İkisi birlikte "eksiz alanlar gerçekten <yil> verisi" demektir.
|
||
*/
|
||
export function yilKilidi(db: Db, yil: number, ornek: KilavuzKaydi[]): void {
|
||
const onceki = yil - 1;
|
||
const bul = db.prepare(`SELECT sira${onceki} AS s FROM programs WHERE id = ?`);
|
||
let kiyas1 = 0;
|
||
let esit1 = 0;
|
||
let kiyas0 = 0;
|
||
let esit0 = 0;
|
||
for (const r of ornek) {
|
||
if (!r.kilavuzKodu) continue;
|
||
let satir = bul.get(String(r.kilavuzKodu)) as { s: number | null } | undefined;
|
||
const eski = eskiKod(r);
|
||
if (!satir && eski) satir = bul.get(eski) as typeof satir;
|
||
if (!satir || satir.s == null) continue;
|
||
const s1 = intOrNull(r.basariSirasi1);
|
||
if (s1 != null) {
|
||
kiyas1++;
|
||
if (s1 === satir.s) esit1++;
|
||
}
|
||
const s0 = intOrNull(r.basariSirasi);
|
||
if (s0 != null) {
|
||
kiyas0++;
|
||
if (s0 === satir.s) esit0++;
|
||
}
|
||
}
|
||
const oran1 = kiyas1 ? esit1 / kiyas1 : 0;
|
||
const oran0 = kiyas0 ? esit0 / kiyas0 : 0;
|
||
console.log(
|
||
`Yıl kilidi: basariSirasi1 = sira${onceki} → ${esit1}/${kiyas1} (%${(oran1 * 100).toFixed(1)}); ` +
|
||
`eksiz basariSirasi = sira${onceki} → ${esit0}/${kiyas0} (%${(oran0 * 100).toFixed(1)})`
|
||
);
|
||
if (kiyas1 < KILIT_ASGARI_ORNEK) {
|
||
dur(
|
||
`Yıl kilidi için yeterli karşılaştırma yok (${kiyas1} < ${KILIT_ASGARI_ORNEK}). ` +
|
||
`DB'de sira${onceki} dolu mu, doğru DB mi?`
|
||
);
|
||
}
|
||
if (oran0 >= 0.5) {
|
||
dur(
|
||
`API'nin eksiz alanları hâlâ ${onceki} verisi (DB ile %${(oran0 * 100).toFixed(1)} aynı). ` +
|
||
`${yil} yerleştirme sonuçları henüz yayımlanmamış; bugün koşmak ${yil} kolonuna ${onceki} verisi yazar.`
|
||
);
|
||
}
|
||
if (oran1 < KILIT_ESIK) {
|
||
dur(
|
||
`API'nin "…1" alanları DB'deki ${onceki} verisiyle eşleşmiyor (%${(oran1 * 100).toFixed(1)} < %95). ` +
|
||
`API yıl düzeni değişmiş olabilir; --yil ${yil} ile yazmak yanlış kolonu doldurur.`
|
||
);
|
||
}
|
||
}
|
||
|
||
// ── Ana akış ─────────────────────────────────────────────────────────────
|
||
|
||
async function apidenCek(
|
||
yil: number,
|
||
hamDizin: string,
|
||
ilkSayfaKontrol: (ornek: KilavuzKaydi[]) => void
|
||
): Promise<HamOkuma & { hamDosya: string }> {
|
||
fs.mkdirSync(hamDizin, { recursive: true });
|
||
const tarih = new Date().toISOString().slice(0, 10);
|
||
const hamDosya = path.join(hamDizin, `${yil}-${tarih}.jsonl.gz`);
|
||
const gecici = `${hamDosya}.tmp`;
|
||
|
||
const first = await fetchPage(0);
|
||
const kayitlar = first.content.map(inceKayit);
|
||
// Kilit tutmazsa hiçbir şey yazılmadan ve kaynağa 1 istekle durulur.
|
||
ilkSayfaKontrol(kayitlar);
|
||
|
||
console.log(
|
||
`Toplam ${first.totalElements} kayıt, ${first.totalPages} sayfa çekilecek ` +
|
||
`(~${Math.ceil((first.totalPages * (DELAY_MS + 1500)) / 1000)} sn)…`
|
||
);
|
||
const gzip = zlib.createGzip();
|
||
const cikti = fs.createWriteStream(gecici);
|
||
gzip.pipe(cikti);
|
||
const bitti = new Promise<void>((resolve, reject) => {
|
||
cikti.on("finish", resolve);
|
||
cikti.on("error", reject);
|
||
gzip.on("error", reject);
|
||
});
|
||
const yaz = (obj: unknown) => gzip.write(`${JSON.stringify(obj)}\n`);
|
||
|
||
yaz({
|
||
_meta: {
|
||
yil,
|
||
cekimTarihi: new Date().toISOString(),
|
||
kaynak: API,
|
||
totalElements: first.totalElements,
|
||
totalPages: first.totalPages,
|
||
sayfaBoyu: PAGE_SIZE,
|
||
},
|
||
});
|
||
for (const r of first.content) yaz(r);
|
||
for (let p = 1; p < first.totalPages; p++) {
|
||
await sleep(DELAY_MS);
|
||
const page = await fetchPage(p);
|
||
for (const r of page.content) {
|
||
yaz(r);
|
||
kayitlar.push(inceKayit(r));
|
||
}
|
||
process.stdout.write(`\rsayfa ${p + 1}/${first.totalPages}`);
|
||
}
|
||
console.log();
|
||
gzip.end();
|
||
await bitti;
|
||
fs.renameSync(gecici, hamDosya);
|
||
console.log(`Ham veri: ${hamDosya}`);
|
||
return { toplam: first.totalElements, kayitlar, hamDosya };
|
||
}
|
||
|
||
function oncekiYilSaglamasi(db: Db, yil: number, azamiRowid: number): string {
|
||
const kolonlar: string[] = [];
|
||
for (let y = ILK_YIL; y < yil; y++) {
|
||
for (const olcu of ["sira", "puan", "kontenjan", "yerlesen"]) {
|
||
kolonlar.push(`COUNT(${olcu}${y})`, `TOTAL(${olcu}${y})`);
|
||
}
|
||
}
|
||
const r = db
|
||
.prepare(`SELECT COUNT(*), ${kolonlar.join(", ")} FROM programs WHERE rowid <= ?`)
|
||
.raw()
|
||
.get(azamiRowid) as number[];
|
||
return r.join("|");
|
||
}
|
||
|
||
async function main() {
|
||
const yil = yilArgumani();
|
||
const dbYolu = dbArgumani();
|
||
const hamArg = argOku("ham");
|
||
const hamDizin = path.resolve(process.cwd(), argOku("ham-dizin") ?? path.join("data", "ham"));
|
||
|
||
const db = dbAc(dbYolu);
|
||
console.log(`DB: ${dbYolu}\nYıl: ${yil}`);
|
||
|
||
// Şema (idempotent). Yeni programlarda …1/…2/…3 yazılacağı için önceki üç
|
||
// yılın kolonları da garanti edilir (normalde zaten vardır).
|
||
const eklenen = yokatlasGoc(db, yil);
|
||
for (let fark = 1; fark <= 3; fark++) {
|
||
if (yil - fark >= ILK_YIL) eklenen.push(...yilKolonlariniGarantiEt(db, yil - fark));
|
||
}
|
||
if (eklenen.length) console.log(`Göç: ${eklenen.join(", ")}`);
|
||
|
||
// ── Veri: API ya da ham dosya ──────────────────────────────────────────
|
||
let toplam: number;
|
||
let kayitlar: KilavuzKaydi[];
|
||
let hamDosya: string;
|
||
if (hamArg) {
|
||
hamDosya = path.resolve(process.cwd(), hamArg);
|
||
console.log(`Kaynak: ham dosya (API'ye gidilmiyor) → ${hamDosya}`);
|
||
({ toplam, kayitlar } = await hamOku(hamDosya, yil));
|
||
yilKilidi(db, yil, kayitlar.slice(0, PAGE_SIZE));
|
||
} else {
|
||
({ toplam, kayitlar, hamDosya } = await apidenCek(yil, hamDizin, (ornek) =>
|
||
yilKilidi(db, yil, ornek)
|
||
));
|
||
}
|
||
|
||
// ── Bütünlük: benzersiz kod = totalElements ────────────────────────────
|
||
const benzersiz = new Map<string, KilavuzKaydi>();
|
||
let kodsuz = 0;
|
||
for (const r of kayitlar) {
|
||
if (!r.kilavuzKodu) {
|
||
kodsuz++;
|
||
continue;
|
||
}
|
||
benzersiz.set(String(r.kilavuzKodu), r);
|
||
}
|
||
console.log(
|
||
`Çekilen: ${kayitlar.length} kayıt, benzersiz kod: ${benzersiz.size}, kodsuz: ${kodsuz}, API toplamı: ${toplam}`
|
||
);
|
||
if (benzersiz.size + kodsuz !== toplam || kayitlar.length !== toplam) {
|
||
dur(
|
||
`Benzersiz kod sayısı (${benzersiz.size}) + kodsuz (${kodsuz}) ≠ API toplamı (${toplam}). ` +
|
||
"Sayfalama sırasında kayıt kaymış/yinelenmiş; DB'ye HİÇBİR ŞEY yazılmadı. Tekrar dene."
|
||
);
|
||
}
|
||
|
||
// ── Yazım: tek transaction ─────────────────────────────────────────────
|
||
const update = db.prepare(`
|
||
UPDATE programs
|
||
SET sira${yil} = ?, puan${yil} = ?, kontenjan${yil} = ?, yerlesen${yil} = ?,
|
||
son_kilavuz_yili = ?, eski_kod = COALESCE(?, eski_kod)
|
||
WHERE id = ?
|
||
`);
|
||
const gecmisYillar = [1, 2, 3].filter((f) => yil - f >= ILK_YIL);
|
||
const insert = db.prepare(`
|
||
INSERT INTO programs (id, isim, universite, unitur, il, fakulte, tur, sure, onlisans,
|
||
sira${yil}, puan${yil}, kontenjan${yil}, yerlesen${yil},
|
||
son_kilavuz_yili, eski_kod
|
||
${gecmisYillar.map((f) => `, sira${yil - f}, puan${yil - f}, kontenjan${yil - f}`).join("")})
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?${gecmisYillar.map(() => ", ?, ?, ?").join("")})
|
||
`);
|
||
const exists = db.prepare("SELECT 1 FROM programs WHERE id = ?");
|
||
|
||
// Yeni satırda yazım/sözlük tutarlılığı — TAHMİN YOK, yalnız DB'de zaten var
|
||
// olan yazımı yeniden kullanma:
|
||
// - unitur: API sözlüğü (DEVLET/VAKIF/KKTC/YURTDIŞI) DB sözlüğünden (VAKIF MYO,
|
||
// YURTDISI KAMU…) farklı. Aynı üniversitenin mevcut satırlarında TEK bir
|
||
// unitur varsa o kullanılır; yoksa API değeri olduğu gibi yazılır. Aksi
|
||
// hâlde aynı kurumda iki unitur oluşur ve uniturOnar haklı olarak durur.
|
||
// - fakulte: API 2026'da BÜYÜK HARF veriyor; aynı üniversitede büyük harfe
|
||
// çevrilince aynı olan mevcut yazım varsa o kullanılır.
|
||
const bazAd = (ad: string) => ad.replace(/\s*\([^)]*\)\s*$/, "").trim();
|
||
const uniturlar = new Map<string, Set<string>>();
|
||
const fakulteler = new Map<string, Map<string, string>>();
|
||
for (const s of db
|
||
.prepare("SELECT DISTINCT universite, unitur, fakulte FROM programs")
|
||
.all() as { universite: string; unitur: string | null; fakulte: string | null }[]) {
|
||
const baz = bazAd(s.universite);
|
||
if (s.unitur) {
|
||
for (const k of [s.universite, baz]) {
|
||
const set = uniturlar.get(k) ?? new Set<string>();
|
||
set.add(s.unitur);
|
||
uniturlar.set(k, set);
|
||
}
|
||
}
|
||
if (s.fakulte) {
|
||
const m = fakulteler.get(baz) ?? new Map<string, string>();
|
||
const anahtar = s.fakulte.toLocaleUpperCase("tr-TR");
|
||
// Aynı anahtara iki farklı yazım düşerse belirsiz → yeniden kullanma
|
||
m.set(anahtar, m.has(anahtar) && m.get(anahtar) !== s.fakulte ? "" : s.fakulte);
|
||
fakulteler.set(baz, m);
|
||
}
|
||
}
|
||
const uniturSec = (universite: string, api: string | null): string | null => {
|
||
for (const k of [universite, bazAd(universite)]) {
|
||
const set = uniturlar.get(k);
|
||
if (set?.size === 1) return [...set][0];
|
||
}
|
||
return api;
|
||
};
|
||
const fakulteSec = (universite: string, api: string | null): string | null => {
|
||
if (!api) return null;
|
||
const mevcut = fakulteler.get(bazAd(universite))?.get(api.toLocaleUpperCase("tr-TR"));
|
||
return mevcut || api;
|
||
};
|
||
|
||
let updated = 0;
|
||
let inserted = 0;
|
||
const atlananlar: string[] = [];
|
||
|
||
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);
|
||
|
||
for (const [id, r] of benzersiz) {
|
||
const tur = TUR_MAP[r.puanTuru?.trim() ?? ""];
|
||
// Puan türü yalnız YENİ satır için gerekir; mevcut satırın türü DB'de var.
|
||
// Kaynak türü vermiyorsa yeni satır UYDURULMAZ, atlanır ve raporlanır.
|
||
if (!tur && !exists.get(id)) {
|
||
atlananlar.push(`${id} ${r.universiteAdi} / ${r.birimAdi} (puanTuru="${r.puanTuru}")`);
|
||
continue;
|
||
}
|
||
const vals = [
|
||
intOrNull(r.basariSirasi),
|
||
realOrNull(r.minPuan),
|
||
intOrNull(r.kontenjan),
|
||
yerlesen(r),
|
||
];
|
||
const eski = eskiKod(r);
|
||
if (exists.get(id)) {
|
||
update.run(...vals, yil, eski, id);
|
||
updated++;
|
||
} else {
|
||
const gecmis = gecmisAlanlari(r).filter((g) => yil - g.fark >= ILK_YIL);
|
||
// "ÜNİVERSİTESİ- CERRAHPAŞA" gibi tire etrafı boşluklu varyantlar
|
||
// duplikat kurum adı üretmesin
|
||
const universite = uniAdiNormalize(r.universiteAdi);
|
||
insert.run(
|
||
id,
|
||
r.birimAdi,
|
||
universite,
|
||
uniturSec(universite, r.universiteTuru ?? null),
|
||
r.ilAdi ?? null,
|
||
fakulteSec(universite, r.fymkAdi ?? null),
|
||
tur,
|
||
r.ogrenimSuresi ?? null,
|
||
r.birimTuruId === 47 ? 1 : 0,
|
||
...vals,
|
||
yil,
|
||
eski,
|
||
...gecmis.flatMap((g) => [g.sira, g.puan, g.kontenjan])
|
||
);
|
||
inserted++;
|
||
}
|
||
}
|
||
|
||
const sonra = oncekiYilSaglamasi(db, yil, azamiRowid);
|
||
if (once !== sonra) {
|
||
dur(
|
||
"Mevcut satırların önceki yıl kolonları değişti — bu betik onlara dokunmamalı. Geri alındı.\n" +
|
||
`önce : ${once}\nsonra: ${sonra}`
|
||
);
|
||
}
|
||
const gorulen = (
|
||
db.prepare("SELECT COUNT(*) AS n FROM programs WHERE son_kilavuz_yili = ?").get(yil) as { n: number }
|
||
).n;
|
||
const beklenen = toplam - kodsuz - atlananlar.length;
|
||
if (gorulen !== beklenen) {
|
||
dur(
|
||
`son_kilavuz_yili=${yil} satır sayısı ${gorulen}, beklenen ${beklenen} ` +
|
||
`(API ${toplam} − atlanan ${kodsuz + atlananlar.length}). Geri alındı.`
|
||
);
|
||
}
|
||
|
||
metaYaz(db, `refresh_${yil}_api_toplam`, toplam);
|
||
metaYaz(db, `refresh_${yil}_atlanan`, kodsuz + atlananlar.length);
|
||
metaYaz(db, `refresh_${yil}_tarih`, new Date().toISOString());
|
||
metaYaz(db, `refresh_${yil}_ham`, path.basename(hamDosya));
|
||
})();
|
||
|
||
console.log(
|
||
`Güncellenen: ${updated}, eklenen: ${inserted}, atlanan: ${kodsuz + atlananlar.length}`
|
||
);
|
||
for (const a of atlananlar) console.log(` atlandı: ${a}`);
|
||
|
||
// API'nin universiteTuru döndürmediği kayıtlar devlet/vakıf filtrelerinden
|
||
// düşmesin: her tazelemeden sonra unitur onarımı koşulur (idempotent).
|
||
db.transaction(() => uniturOnar(db))();
|
||
|
||
// Sorgu yolunun ifade indeksi — ifade veri-yillari.ts'ten; eski yılınki DROP.
|
||
console.log(`Efektif indeks: ${efektifIndeksiKur(db)}`);
|
||
|
||
const stats = db
|
||
.prepare(
|
||
`SELECT COUNT(*) AS toplam,
|
||
SUM(son_kilavuz_yili = ${yil}) AS guncel,
|
||
COUNT(sira${yil}) AS sirali
|
||
FROM programs`
|
||
)
|
||
.get() as { toplam: number; guncel: number; sirali: number };
|
||
console.log(
|
||
`DB toplam: ${stats.toplam} program; ${yil} kılavuzunda: ${stats.guncel}; ` +
|
||
`${yil} sıralaması olan: ${stats.sirali}`
|
||
);
|
||
db.pragma("wal_checkpoint(TRUNCATE)");
|
||
db.close();
|
||
console.log(
|
||
`\nSıradaki adım: pnpm gecmis-doldur --yil ${yil} --db <db> --ham ${path.relative(process.cwd(), hamDosya)}`
|
||
);
|
||
}
|
||
|
||
if (process.argv[1]?.endsWith("refresh.ts")) calistir(main);
|