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:
79
scripts/eval/varyant-denetim.ts
Normal file
79
scripts/eval/varyant-denetim.ts
Normal file
@@ -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);
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
40
src/lib/program-varyant.ts
Normal file
40
src/lib/program-varyant.ts
Normal 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;
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user