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 { KATEGORILER, kategoriBul } from "./kategoriler";
|
||||||
import { EFEKTIF_SIRA_SQL, ILK_YIL, SON_YIL, YILLAR } from "./veri-yillari";
|
import { EFEKTIF_SIRA_SQL, ILK_YIL, SON_YIL, YILLAR } from "./veri-yillari";
|
||||||
import { turkishSlugify } from "./slug";
|
import { turkishSlugify } from "./slug";
|
||||||
|
import { varyantEtiketi } from "./program-varyant";
|
||||||
import {
|
import {
|
||||||
PUAN_TURLERI,
|
PUAN_TURLERI,
|
||||||
type ArsivProgram,
|
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
|
// 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
|
// 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).
|
// `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,
|
export const SELECT_COLS = `id, isim, universite, unitur, il, fakulte, tur, sure,
|
||||||
${YILLAR.map((yil) => `sira${yil} AS s${yil}`).join(", ")},
|
${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.
|
// 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.
|
// İfade indeks sözleşmesidir; tek kaynağı lib/veri-yillari.ts.
|
||||||
export const EFEKTIF_SIRA = EFEKTIF_SIRA_SQL;
|
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<
|
export type ProgramHamSatiri = Omit<
|
||||||
Program,
|
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. */
|
/** Ham DB satırını yılsız `Program`'a çevirir. */
|
||||||
export function programSatiri(row: ProgramHamSatiri): Program {
|
export function programSatiri(row: ProgramHamSatiri): Program {
|
||||||
@@ -72,6 +86,7 @@ export function programSatiri(row: ProgramHamSatiri): Program {
|
|||||||
puanSon: row.puanSon,
|
puanSon: row.puanSon,
|
||||||
kontenjanSon: row.kontenjanSon,
|
kontenjanSon: row.kontenjanSon,
|
||||||
yerlesenSon: row.yerlesenSon,
|
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,
|
unitur: p.unitur,
|
||||||
il: p.il,
|
il: p.il,
|
||||||
dilim: p.dilim,
|
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
|
// Anahtarlar yıl taşır (y<yıl>, kontenjan<yıl>): model rakamın hangi
|
||||||
// yıla ait olduğunu anahtardan okur.
|
// yıla ait olduğunu anahtardan okur.
|
||||||
sira: Object.fromEntries(
|
sira: Object.fromEntries(
|
||||||
|
|||||||
@@ -21,6 +21,13 @@ export type Program = {
|
|||||||
puanSon: number | null;
|
puanSon: number | null;
|
||||||
kontenjanSon: number | null;
|
kontenjanSon: number | null;
|
||||||
yerlesenSon: 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 = {
|
export const PUAN_TURLERI = {
|
||||||
|
|||||||
Reference in New Issue
Block a user