v2 #10
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);
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
SATIR_BUTON_BASLIK,
|
||||
SATIR_BUTON_HUCRE,
|
||||
SATIR_BUTON_KABI,
|
||||
SATIR_CIP,
|
||||
SATIR_GRUP,
|
||||
SATIR_IKINCIL_LINK,
|
||||
SATIR_META,
|
||||
@@ -242,6 +243,10 @@ export function BolumProgramListesi({
|
||||
)}
|
||||
<span className={SATIR_META}>
|
||||
<TurCipi unitur={p.unitur} />
|
||||
{/* Burs/dil/öğretim türü: aynı adlı satırları ayırt eder (§4.15 çip) */}
|
||||
{p.varyant ? (
|
||||
<span className={SATIR_CIP}>{p.varyant}</span>
|
||||
) : null}
|
||||
{p.il ? (
|
||||
<Link
|
||||
href={ilSayfaHref(p.il)}
|
||||
|
||||
@@ -107,6 +107,10 @@ export function UniversiteProgramTablosu({
|
||||
<span className={SATIR_CIP}>
|
||||
{TUR_ETIKET[p.tur] ?? p.tur}
|
||||
</span>
|
||||
{/* Burs/dil/öğretim türü: aynı adlı satırları ayırt eder (§4.15 çip) */}
|
||||
{p.varyant ? (
|
||||
<span className={SATIR_CIP}>{p.varyant}</span>
|
||||
) : null}
|
||||
{p.sure ? (
|
||||
<span className="shrink-0">{p.sure} yıl</span>
|
||||
) : null}
|
||||
|
||||
@@ -452,6 +452,10 @@ function CekmeceSatiri({
|
||||
>
|
||||
{trBaslikDuzeni(tercih.universite)}
|
||||
</Link>
|
||||
{/* Burs/dil/öğretim türü: aynı adlı satırları ayırt eder */}
|
||||
{tercih.varyant ? (
|
||||
<span className="shrink-0 text-slate-500">· {tercih.varyant}</span>
|
||||
) : null}
|
||||
</span>
|
||||
{tercih.risk || tercih.efektifSira != null ? (
|
||||
<span className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-slate-500">
|
||||
|
||||
@@ -28,6 +28,7 @@ type ProgramOzet = Pick<
|
||||
| "unitur"
|
||||
| "tur"
|
||||
| "efektifSira"
|
||||
| "varyant"
|
||||
>;
|
||||
|
||||
const PROFILSIZ_EKLEME_TOAST_ID = "profilsiz-ekleme";
|
||||
|
||||
@@ -55,6 +55,7 @@ import {
|
||||
SATIR_BUTON_HUCRE,
|
||||
SATIR_BUTON_KABI,
|
||||
SATIR_ALT_GRUP,
|
||||
SATIR_CIP,
|
||||
SATIR_IKINCIL_LINK,
|
||||
SATIR_META,
|
||||
SATIR_MOBIL_SAYI,
|
||||
@@ -373,6 +374,10 @@ function ProgramSatiri({
|
||||
unitur={p.unitur}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
{/* Burs/dil/öğretim türü: aynı adlı satırları ayırt eder (§4.15 çip) */}
|
||||
{p.varyant ? (
|
||||
<span className={SATIR_CIP}>{p.varyant}</span>
|
||||
) : null}
|
||||
{p.il ? (
|
||||
<Link
|
||||
href={ilSayfaHref(p.il)}
|
||||
|
||||
@@ -29,6 +29,7 @@ export type BekleyenProgram = {
|
||||
| "unitur"
|
||||
| "tur"
|
||||
| "efektifSira"
|
||||
| "varyant"
|
||||
>;
|
||||
kaynak: ManuelKaynak;
|
||||
adaySira?: number;
|
||||
@@ -271,11 +272,16 @@ export function programEkleIste(opts: {
|
||||
}
|
||||
|
||||
const profil = profilsiz ? null : (durum.profil ?? tercihProfiliOku());
|
||||
const tercih = programdanManuelTercih(
|
||||
// programdanManuelTercih (liste-store) varyantı bilmez; etiket burada
|
||||
// kopyalanır ki çekmece aynı adlı satırları ayırt edebilsin.
|
||||
const tercih = {
|
||||
...programdanManuelTercih(
|
||||
program,
|
||||
adaySira ?? profil?.sira ?? null,
|
||||
kaynak,
|
||||
);
|
||||
),
|
||||
varyant: program.varyant ?? null,
|
||||
};
|
||||
const ok = ekle(tercih);
|
||||
if (!ok) {
|
||||
return {
|
||||
|
||||
@@ -30,6 +30,8 @@ export type ManuelTercih = {
|
||||
/** Ekleme / profil anındaki aday sıralamasına göre risk */
|
||||
risk: RiskSeviyesi | null;
|
||||
kaynak?: ManuelKaynak;
|
||||
/** Burs/dil/öğretim türü etiketi (Program.varyant); eski kayıtlarda yok */
|
||||
varyant?: string | null;
|
||||
};
|
||||
|
||||
/** Güvenilmeyen JSON'u (localStorage veya sunucu kaydı) listeye süzer. */
|
||||
@@ -66,6 +68,7 @@ export function dogrula(ham: unknown): ManuelTercih[] {
|
||||
: null,
|
||||
risk,
|
||||
kaynak,
|
||||
varyant: typeof kayit.varyant === "string" ? kayit.varyant : null,
|
||||
});
|
||||
if (sonuc.length >= MANUEL_LISTE_MAX) break;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ export function programOzetle(p: Program): ProgramListeSatiri {
|
||||
efektifSira: p.efektifSira,
|
||||
kontenjanSon: p.kontenjanSon,
|
||||
yerlesenSon: p.yerlesenSon,
|
||||
varyant: p.varyant,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
SATIR_BUTON_HUCRE,
|
||||
SATIR_DUGME_BOYU,
|
||||
SATIR_ALT_GRUP,
|
||||
SATIR_CIP,
|
||||
SATIR_IKINCIL_LINK,
|
||||
SATIR_META,
|
||||
SATIR_MOBIL_SAYI,
|
||||
@@ -267,6 +268,10 @@ export function RaporListesi({
|
||||
unitur={p?.unitur}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
{/* Burs/dil/öğretim türü: aynı adlı satırları ayırt eder (§4.15 çip) */}
|
||||
{p?.varyant ? (
|
||||
<span className={SATIR_CIP}>{p.varyant}</span>
|
||||
) : null}
|
||||
{p?.il ? (
|
||||
<Link
|
||||
href={ilSayfaHref(p.il)}
|
||||
|
||||
@@ -2,6 +2,8 @@ import "server-only";
|
||||
import { desc, eq, sql } from "drizzle-orm";
|
||||
import { appDb, schema } from "@/lib/appdb";
|
||||
import { anahtarVar } from "@/lib/ai/client";
|
||||
import { programOzellikleriGetir } from "@/lib/program-ozellik";
|
||||
import { varyantEtiketi } from "@/lib/program-varyant";
|
||||
import type { RaporSonuc } from "./types/rapor";
|
||||
|
||||
// Rapor feature'ının sorgu kapısı: kayıtlı rapor, sohbet geçmişi ve girişsiz
|
||||
@@ -37,9 +39,23 @@ export async function kullanicininRaporu(userId: string) {
|
||||
where: eq(schema.reports.userId, userId),
|
||||
});
|
||||
if (satir?.result && bayatMockMu(satir.result as RaporSonuc)) return null;
|
||||
if (satir?.result) varyantlariEkle(satir.result as RaporSonuc);
|
||||
return satir ?? null;
|
||||
}
|
||||
|
||||
// Kayıtlı raporun program satırlarına burs/dil/öğretim türü etiketi (okuma
|
||||
// anında, tek sorgu): rapor JSON'u üretim anında bunu taşımıyor, eski
|
||||
// kayıtlar da bu yoldan kazanır. yokatlas.db salt okunur; app.db'ye yazılmaz.
|
||||
function varyantlariEkle(sonuc: RaporSonuc): void {
|
||||
const idler = Object.keys(sonuc.programlar ?? {});
|
||||
if (idler.length === 0) return;
|
||||
const ozellikler = programOzellikleriGetir(idler);
|
||||
for (const id of idler) {
|
||||
const o = ozellikler[id];
|
||||
sonuc.programlar[id].varyant = o ? varyantEtiketi(o) : null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kullanıcının danışman sohbet geçmişi: en YENİ 100 mesaj, eskiden yeniye.
|
||||
* created_at saniye çözünürlüklü; aynı saniyedeki soru-cevap sırası rowid ile
|
||||
|
||||
@@ -58,6 +58,9 @@ export type RaporSonuc = {
|
||||
efektifSira?: number | null;
|
||||
siraGecmisi?: (number | null)[];
|
||||
ilkYil?: number;
|
||||
/** Burs/dil/öğretim türü etiketi; DB'ye yazılmaz, okuma anında
|
||||
* program_ozellik'ten eklenir (rapor-queries kullanicininRaporu) */
|
||||
varyant?: string | null;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
@@ -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