From 5c583ac8b4bfcc1f009c6b44f758aa23bebd6bad Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Wed, 7 Oct 2026 23:12:03 +0300 Subject: [PATCH] =?UTF-8?q?feat(veri):=20Program.varyant=20=E2=80=94=20bur?= =?UTF-8?q?s/dil/=C3=B6=C4=9Fretim=20t=C3=BCr=C3=BC=20etiketi=20tek=20kayn?= =?UTF-8?q?aktan=20(program=5Fozellik)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit programs.isim burs/dil varyantını taşımıyor (Başkent Tıp 6 satırda da "Tıp"); ayırt edici alanlar program_ozellik.burs_orani / ogrenim_dili / ogrenim_turu. SELECT_COLS üç ilişkili alt sorgu kazanır, programSatiri varyantEtiketi ile "Burslu · İngilizce" gibi tek etiket üretir; devlet / Türkçe / örgün programda null. Havuz JSON'una da girer: model aynı adlı satırları ayırt edebilsin. Tablo yokluğu koruması bilinçli yok: data/yokatlas.db depoda ve Docker seed'inde (Dockerfile) program_ozellik tablosuyla geliyor. programs_arsiv sorguları dokunulmadı (arşivde özellik yok). Test: pnpm tsx scripts/eval/varyant-denetim.ts (salt okur, çıkış 0). Co-Authored-By: Claude Fable 5.1 --- scripts/eval/varyant-denetim.ts | 79 +++++++++++++++++++++++++++++++++ src/lib/db.ts | 23 ++++++++-- src/lib/program-varyant.ts | 40 +++++++++++++++++ src/lib/rapor-havuzu.ts | 3 ++ src/types/yokatlas.ts | 7 +++ 5 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 scripts/eval/varyant-denetim.ts create mode 100644 src/lib/program-varyant.ts diff --git a/scripts/eval/varyant-denetim.ts b/scripts/eval/varyant-denetim.ts new file mode 100644 index 0000000..310a67b --- /dev/null +++ b/scripts/eval/varyant-denetim.ts @@ -0,0 +1,79 @@ +/** + * Program varyant etiketinin MEKANİK testi — parasız, anahtarsız, salt okur. + * + * pnpm tsx scripts/eval/varyant-denetim.ts + * + * react-server koşulu gerekmez: db.ts ve program-varyant.ts saf/sqlite. + * Doğrulananlar: (1) varyantEtiketi birim kuralları, (2) Başkent Tıp'ın 6 + * satırının etiketleri birbirinden farklı ve hiçbiri null, (3) dilimAra'nın + * döndürdüğü her satırda `varyant` tanımlı (SELECT_COLS tüm yolu kapsıyor), + * (4) devlet/Türkçe/örgün programda null. data/yokatlas.db'ye yazılmaz. + * Beklenti tutmazsa çıkış kodu 1. + */ +import { dilimAra, programGetir } from "../../src/lib/db"; +import { varyantEtiketi, type VaryantGirdisi } from "../../src/lib/program-varyant"; + +let hata = 0; +function bekle(ad: string, kosul: boolean, detay = ""): void { + console.log(`${kosul ? "✔" : "✘"} ${ad}${detay ? ` — ${detay}` : ""}`); + if (!kosul) hata++; +} + +// 1. Birim: etiket kuralları +const birim: [VaryantGirdisi, string | null][] = [ + [{ bursOrani: "Burslu", ogrenimDili: "İngilizce", ogrenimTuru: "Örgün Öğretim" }, "Burslu · İngilizce"], + [{ bursOrani: null, ogrenimDili: "Türkçe", ogrenimTuru: "Örgün Öğretim" }, null], + [{ bursOrani: "Ücretli", ogrenimDili: "Türkçe", ogrenimTuru: "İkinci Öğretim" }, "Ücretli · İkinci Öğretim"], + [{ bursOrani: "%50 İndirimli", ogrenimDili: null, ogrenimTuru: null }, "%50 İndirimli"], + // DB'de burs_orani boş string de geliyor (devlet); null gibi davranır + [{ bursOrani: "", ogrenimDili: "Türkçe", ogrenimTuru: "Uzaktan Öğretim" }, "Uzaktan Öğretim"], +]; +for (const [girdi, beklenen] of birim) { + const sonuc = varyantEtiketi(girdi); + bekle( + `birim ${JSON.stringify(girdi)}`, + sonuc === beklenen, + `beklenen ${JSON.stringify(beklenen)}, gelen ${JSON.stringify(sonuc)}`, + ); +} + +// 2. Başkent Tıp ×6: isim hepsinde "Tıp", varyant hepsinde farklı +const BASKENT_TIP = ["200610657", "200610666", "200611488", "200690357", "200690358", "200690359"]; +const baskent = BASKENT_TIP.map((id) => ({ id, p: programGetir(id) })); +for (const { id, p } of baskent) { + bekle( + `programGetir(${id}) varyant dolu`, + p != null && p.varyant != null, + p ? `${p.universite} / ${p.isim} → ${p.varyant}` : "bulunamadı", + ); +} +const etiketler = baskent.map(({ p }) => p?.varyant ?? null); +bekle( + "Başkent Tıp 6 varyant birbirinden farklı", + new Set(etiketler).size === BASKENT_TIP.length, + etiketler.join(" | "), +); + +// 3. dilimAra yolu: her satırda varyant tanımlı (string ya da null) +const { programlar } = dilimAra(100000, "say", "dengeli", { limit: 50 }); +const tanimsiz = programlar.filter( + (p) => !("varyant" in p) || (p.varyant !== null && typeof p.varyant !== "string"), +); +bekle( + `dilimAra 50 satırda varyant tanımlı (${programlar.length} satır)`, + programlar.length > 0 && tanimsiz.length === 0, + tanimsiz.map((p) => p.id).join(","), +); +const etiketli = programlar.filter((p) => p.varyant).length; +console.log(` (bilgi) dilimAra örneğinde etiketli satır: ${etiketli}/${programlar.length}`); + +// 4. Devlet / Türkçe / örgün → null (Marmara Hukuk) +const marmara = programGetir("107210332"); +bekle( + "107210332 Marmara Hukuk varyant null", + marmara != null && marmara.varyant === null, + marmara ? `${marmara.universite} / ${marmara.isim} → ${JSON.stringify(marmara.varyant)}` : "bulunamadı", +); + +console.log(hata === 0 ? "\nVaryant denetimi: TEMİZ" : `\nVaryant denetimi: ${hata} hata`); +process.exit(hata === 0 ? 0 : 1); diff --git a/src/lib/db.ts b/src/lib/db.ts index 5433355..f6d24f9 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -5,6 +5,7 @@ import Database from "better-sqlite3"; import { KATEGORILER, kategoriBul } from "./kategoriler"; import { EFEKTIF_SIRA_SQL, ILK_YIL, SON_YIL, YILLAR } from "./veri-yillari"; import { turkishSlugify } from "./slug"; +import { varyantEtiketi } from "./program-varyant"; import { PUAN_TURLERI, type ArsivProgram, @@ -37,20 +38,33 @@ export function getDb(): Database.Database { // Yıl ekli kolonlar yalnız burada, `YILLAR`'dan üretilerek adlandırılır; DB // dışına yılsız `Program` çıkar. SELECT_COLS ile okunan her satır // `programSatiri` eşleyicisinden geçmelidir (ham satır `Program` değildir). +// Burs/dil/öğretim türü `program_ozellik`'ten ilişkili alt sorguyla gelir +// (id PRIMARY KEY). Bütün SELECT_COLS kullanımları takma adsız `FROM programs` +// olduğu için `programs.id` çözümlenir; `programs_arsiv` (ARSIV_SELECT) bu +// kolonları almaz (arşivde özellik yok). Tablo yokluğu koruması gerekmez: +// data/yokatlas.db depoda ve Docker seed'inde tabloyla gelir. export const SELECT_COLS = `id, isim, universite, unitur, il, fakulte, tur, sure, ${YILLAR.map((yil) => `sira${yil} AS s${yil}`).join(", ")}, - puan${SON_YIL} AS puanSon, kontenjan${SON_YIL} AS kontenjanSon, yerlesen${SON_YIL} AS yerlesenSon`; + puan${SON_YIL} AS puanSon, kontenjan${SON_YIL} AS kontenjanSon, yerlesen${SON_YIL} AS yerlesenSon, + (SELECT burs_orani FROM program_ozellik o WHERE o.id = programs.id) AS bursOrani, + (SELECT ogrenim_dili FROM program_ozellik o WHERE o.id = programs.id) AS ogrenimDili, + (SELECT ogrenim_turu FROM program_ozellik o WHERE o.id = programs.id) AS ogrenimTuru`; // Son yılın sıralaması yoksa (az yerleşen/yeni program) bir önceki yıla düşer. // İfade indeks sözleşmesidir; tek kaynağı lib/veri-yillari.ts. export const EFEKTIF_SIRA = EFEKTIF_SIRA_SQL; -/** SELECT_COLS ile okunan ham satır: sıra kolonları `s` takma adıyla gelir. */ +/** SELECT_COLS ile okunan ham satır: sıra kolonları `s` takma adıyla, + * varyant ise üç ham özellik alanı olarak gelir. */ export type ProgramHamSatiri = Omit< Program, - "siraGecmisi" | "efektifSira" | "siraSon" + "siraGecmisi" | "efektifSira" | "siraSon" | "varyant" > & - Record<`s${number}`, number | null>; + Record<`s${number}`, number | null> & { + bursOrani: string | null; + ogrenimDili: string | null; + ogrenimTuru: string | null; + }; /** Ham DB satırını yılsız `Program`'a çevirir. */ export function programSatiri(row: ProgramHamSatiri): Program { @@ -72,6 +86,7 @@ export function programSatiri(row: ProgramHamSatiri): Program { puanSon: row.puanSon, kontenjanSon: row.kontenjanSon, yerlesenSon: row.yerlesenSon, + varyant: varyantEtiketi(row), }; } diff --git a/src/lib/program-varyant.ts b/src/lib/program-varyant.ts new file mode 100644 index 0000000..c53524f --- /dev/null +++ b/src/lib/program-varyant.ts @@ -0,0 +1,40 @@ +// Program varyant etiketi — saf, DB'siz. `programs.isim` burs/dil/öğretim +// türünü taşımaz (Başkent Tıp 6 satırda da "Tıp"); ayırt edici alanlar +// `program_ozellik.burs_orani / ogrenim_dili / ogrenim_turu`. Bu modül o üç +// alanı satır altında gösterilecek tek etikete çevirir; db.ts `programSatiri` +// ve rapor-queries (kayıtlı rapor) aynı fonksiyonu kullanır. + +export type VaryantGirdisi = { + /** "Burslu" | "%50 İndirimli" | "%25 İndirimli" | "Ücretli"; devlette null/boş */ + bursOrani: string | null; + /** "Türkçe" | "İngilizce" | "İngilizce (%30)" | "Almanca" … */ + ogrenimDili: string | null; + /** "Örgün Öğretim" | "Uzaktan Öğretim" | "Açık Öğretim" | "UOLP" … */ + ogrenimTuru: string | null; +}; + +const VARSAYILAN_DIL = "Türkçe"; +const VARSAYILAN_OGRETIM = "Örgün Öğretim"; +const AYRAC = " · "; + +function dolu(deger: string | null | undefined): string | null { + const metin = deger?.trim(); + return metin ? metin : null; +} + +/** + * Satır altı etiketi: "Burslu · İngilizce", "%50 İndirimli", "İkinci Öğretim", + * "Ücretli · Uzaktan Öğretim". Sıra: burs (varsa, DB'deki hâliyle), dil + * (yalnız Türkçe değilse), öğretim türü (yalnız örgün değilse). Üçü de + * varsayılansa (devlet / Türkçe / örgün) null — satırda hiçbir şey çizilmez. + */ +export function varyantEtiketi(g: VaryantGirdisi): string | null { + const parcalar: string[] = []; + const burs = dolu(g.bursOrani); + if (burs) parcalar.push(burs); + const dil = dolu(g.ogrenimDili); + if (dil && dil !== VARSAYILAN_DIL) parcalar.push(dil); + const ogretim = dolu(g.ogrenimTuru); + if (ogretim && ogretim !== VARSAYILAN_OGRETIM) parcalar.push(ogretim); + return parcalar.length > 0 ? parcalar.join(AYRAC) : null; +} diff --git a/src/lib/rapor-havuzu.ts b/src/lib/rapor-havuzu.ts index 8e0126e..c4d7ad7 100644 --- a/src/lib/rapor-havuzu.ts +++ b/src/lib/rapor-havuzu.ts @@ -433,6 +433,9 @@ export function havuzuKompaktJson(havuz: AdayProgram[]): string { unitur: p.unitur, il: p.il, dilim: p.dilim, + // Burs/dil/öğretim türü: model aynı adlı satırları (Başkent Tıp + // burslu / %50 / ücretli) ayırt etsin, gerekçede "burslu" diyebilsin + ...(p.varyant ? { varyant: p.varyant } : {}), // Anahtarlar yıl taşır (y, kontenjan): model rakamın hangi // yıla ait olduğunu anahtardan okur. sira: Object.fromEntries( diff --git a/src/types/yokatlas.ts b/src/types/yokatlas.ts index d480ba7..0c395d2 100644 --- a/src/types/yokatlas.ts +++ b/src/types/yokatlas.ts @@ -21,6 +21,13 @@ export type Program = { puanSon: number | null; kontenjanSon: number | null; yerlesenSon: number | null; + /** + * Burs / dil / öğretim türü etiketi ("Burslu · İngilizce", "%50 İndirimli", + * "Uzaktan Öğretim"); `program_ozellik`'ten `varyantEtiketi` ile üretilir + * (lib/program-varyant). Devlet/Türkçe/örgün programda null. `isim` aynı + * olan satırları (Başkent Tıp ×6) listede ayırt eden tek alan. + */ + varyant: string | null; }; export const PUAN_TURLERI = {