feat(veri): Program.varyant — burs/dil/öğretim türü etiketi tek kaynaktan (program_ozellik)

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 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-07 23:12:03 +03:00
parent 4d30dd1305
commit 5c583ac8b4
5 changed files with 148 additions and 4 deletions

View File

@@ -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<yıl>` takma adıyla gelir. */
/** SELECT_COLS ile okunan ham satır: sıra kolonları `s<yıl>` 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),
};
}

View File

@@ -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;
}

View File

@@ -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<yıl>, kontenjan<yıl>): model rakamın hangi
// yıla ait olduğunu anahtardan okur.
sira: Object.fromEntries(

View File

@@ -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 = {