Files
kolaytercih/scripts/refresh.ts
bilalgursen a773ac7beb feat(veri): 2026 yükleme hattı — yıl kilitli refresh, geçmiş doldurma, arşiv, kalite kapısı (P-A)
- 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>
2026-09-21 17:03:01 +03:00

608 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);