feat(veri): Program tipi yılsız — db eşleyicisi, lib ve snapshot ilkYil (P-B a)

Program artık siraGecmisi (YILLAR ile hizalı) + efektifSira + siraSon/puanSon/
kontenjanSon/yerlesenSon taşır. db.ts kolonları YILLAR'dan üretip takma adla
seçer, programSatiri eşleyicisi ham satırı Program'a çevirir; SELECT_COLS'un
üç tüketicisi de eşleyiciden geçer. EFEKTIF_SIRA veri-yillari'dan gelir.

- trend: ILK_YIL tek kaynaktan, opsiyonel ilkYil parametresi
- rapor snapshot'ına opsiyonel ilkYil yazılır (eski kayıtlar: okurken ILK_YIL)
- rapor promptu son 4 yılı göndermeye devam eder, anahtarlar dinamik
- tadımlık "son N yıl" filtresi genel: N != VERI_YIL_SAYISI ise servis edilmez
- veri-kapsami VERI_YIL_SAYISI'nı veri-yillari'dan türetir

Bileşenler bir sonraki commit'te yeni tipe geçer (ikisi birlikte derlenir).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-21 17:00:39 +03:00
parent 38e02e0ed9
commit ce40b7c5b3
14 changed files with 195 additions and 106 deletions

View File

@@ -28,8 +28,7 @@ export type BekleyenProgram = {
| "il" | "il"
| "unitur" | "unitur"
| "tur" | "tur"
| "sira2025" | "efektifSira"
| "sira2024"
>; >;
kaynak: ManuelKaynak; kaynak: ManuelKaynak;
adaySira?: number; adaySira?: number;

View File

@@ -8,7 +8,8 @@ import type {
UniturGrubu, UniturGrubu,
} from "@/types/yokatlas"; } from "@/types/yokatlas";
// Tabloda kullanılmayan alanlar (fakulte, puan2025) client'a serileşmesin. // Tabloda kullanılmayan alanlar (fakulte, puanSon, siraSon) client'a
// serileşmesin.
// Alanlar tek tek seçilir: Program'a alan eklenirse ProgramListeSatiri // Alanlar tek tek seçilir: Program'a alan eklenirse ProgramListeSatiri
// üzerinden derleyici burada da karar vermeye zorlar. // üzerinden derleyici burada da karar vermeye zorlar.
export function programOzetle(p: Program): ProgramListeSatiri { export function programOzetle(p: Program): ProgramListeSatiri {
@@ -20,13 +21,10 @@ export function programOzetle(p: Program): ProgramListeSatiri {
il: p.il, il: p.il,
tur: p.tur, tur: p.tur,
sure: p.sure, sure: p.sure,
sira2025: p.sira2025, siraGecmisi: p.siraGecmisi,
sira2024: p.sira2024, efektifSira: p.efektifSira,
sira2023: p.sira2023, kontenjanSon: p.kontenjanSon,
sira2022: p.sira2022, yerlesenSon: p.yerlesenSon,
sira2021: p.sira2021,
kontenjan2025: p.kontenjan2025,
yerlesen2025: p.yerlesen2025,
}; };
} }

View File

@@ -158,10 +158,8 @@ export function sunucuylaBirlestir(sunucuItems: ManuelTercih[]): ManuelTercih[]
return liste; return liste;
} }
export function efektifSiraAl( export function efektifSiraAl(p: Pick<Program, "efektifSira">): number | null {
p: Pick<Program, "sira2025" | "sira2024">, return p.efektifSira ?? null;
): number | null {
return p.sira2025 ?? p.sira2024 ?? null;
} }
/** Program satırını manuel liste kaydına çevirir. */ /** Program satırını manuel liste kaydına çevirir. */
@@ -174,8 +172,7 @@ export function programdanManuelTercih(
| "il" | "il"
| "unitur" | "unitur"
| "tur" | "tur"
| "sira2025" | "efektifSira"
| "sira2024"
>, >,
adaySira?: number | null, adaySira?: number | null,
kaynak: ManuelKaynak = "sonuc", kaynak: ManuelKaynak = "sonuc",

View File

@@ -34,8 +34,13 @@ export type RaporSonuc = {
// ayıklamak için (bkz. features/rapor/rapor-queries.ts) // ayıklamak için (bkz. features/rapor/rapor-queries.ts)
devMock?: boolean; devMock?: boolean;
// Render için havuzdan zenginleştirilmiş program bilgisi // Render için havuzdan zenginleştirilmiş program bilgisi
// efektifSira: COALESCE(sira2025, sira2024) — eski kayıtlı raporlarda yok // efektifSira: üretim anındaki son yılın sırası, yoksa bir önceki yılınki —
// siraGecmisi: [2021..2025] taban sıralamaları — deterministik trend oku // eski kayıtlı raporlarda yok
// siraGecmisi: [ilkYil..] taban sıralamaları — deterministik trend oku.
// Uzunluk üretim anındaki veri penceresidir (eski kayıtlarda 5 eleman);
// okuyan taraf diziyi doldurmaz, yılı `ilkYil + indeks` ile bulur.
// ilkYil: dizinin ilk elemanının yılı — eski kayıtlarda yok (okurken
// lib/veri-yillari ILK_YIL)
programlar: Record< programlar: Record<
string, string,
{ {
@@ -45,6 +50,7 @@ export type RaporSonuc = {
unitur: string | null; unitur: string | null;
efektifSira?: number | null; efektifSira?: number | null;
siraGecmisi?: (number | null)[]; siraGecmisi?: (number | null)[];
ilkYil?: number;
} }
>; >;
}; };

View File

@@ -26,6 +26,7 @@ import {
riskNotuMetni, riskNotuMetni,
} from "../risk"; } from "../risk";
import { trendOzetiMetni } from "../trend"; import { trendOzetiMetni } from "../trend";
import { ILK_YIL } from "../veri-yillari";
// Yapay zekâdan istenen küçük şema (kullanıcı kararı, 6 Ağu 2026): riskNotu // Yapay zekâdan istenen küçük şema (kullanıcı kararı, 6 Ağu 2026): riskNotu
// ve trendOzeti gerçek sıra geçmişinden deterministik üretilir; dilim iskeleti // ve trendOzeti gerçek sıra geçmişinden deterministik üretilir; dilim iskeleti
@@ -141,29 +142,27 @@ function normalizeEt(
function trendOzetiUret(p: AdayProgram): string { function trendOzetiUret(p: AdayProgram): string {
// Cümle tadımlıkla ortak saf fonksiyondan gelir (lib/trend.ts) // Cümle tadımlıkla ortak saf fonksiyondan gelir (lib/trend.ts)
return trendOzetiMetni([ return trendOzetiMetni(p.siraGecmisi);
p.sira2021,
p.sira2022,
p.sira2023,
p.sira2024,
p.sira2025,
]);
} }
function riskNotuUret(p: AdayProgram, adaySira: number): string { function riskNotuUret(p: AdayProgram, adaySira: number): string {
return riskNotuMetni(riskHesapla(p.sira2025 ?? p.sira2024, adaySira)); return riskNotuMetni(riskHesapla(p.efektifSira, adaySira));
} }
/** Havuz programını render için gereken bilgiye indirger. */ /** Havuz programını render için gereken bilgiye indirger. */
function programBilgisi(p: AdayProgram): RaporSonuc["programlar"][string] { function programBilgisi(p: AdayProgram): RaporSonuc["programlar"][string] {
const gecmis = [p.sira2021, p.sira2022, p.sira2023, p.sira2024, p.sira2025]; const gecmis = p.siraGecmisi;
const gecmisVar = gecmis.some((s) => s != null);
return { return {
isim: p.isim, isim: p.isim,
universite: p.universite, universite: p.universite,
il: p.il, il: p.il,
unitur: p.unitur, unitur: p.unitur,
efektifSira: p.sira2025 ?? p.sira2024, efektifSira: p.efektifSira,
siraGecmisi: gecmis.some((s) => s != null) ? gecmis : undefined, siraGecmisi: gecmisVar ? gecmis : undefined,
// Dizinin çapa yılı snapshot'a yazılır: kayıt, veri penceresi sonradan
// uzasa da kendi yıllarını bilir (eski kayıtlarda yok → okurken ILK_YIL).
ilkYil: gecmisVar ? ILK_YIL : undefined,
}; };
} }
@@ -183,7 +182,7 @@ function mockRapor(
// Prod ile aynı hedef hesabı: mock, uyarlanmış dağılımı da birebir taklit eder // Prod ile aynı hedef hesabı: mock, uyarlanmış dağılımı da birebir taklit eder
const hedef = dilimHedefiHesapla(havuz); const hedef = dilimHedefiHesapla(havuz);
const toplamHedef = hedef.hayal + hedef.dengeli + hedef.garanti; const toplamHedef = hedef.hayal + hedef.dengeli + hedef.garanti;
const efektif = (p: AdayProgram) => p.sira2025 ?? p.sira2024 ?? 0; const efektif = (p: AdayProgram) => p.efektifSira ?? 0;
const dilimden = (dilim: AdayProgram["dilim"], adet: number) => const dilimden = (dilim: AdayProgram["dilim"], adet: number) =>
havuz havuz
.filter((p) => p.dilim === dilim) .filter((p) => p.dilim === dilim)
@@ -293,7 +292,7 @@ export async function raporUret(
const havuzMap = new Map(havuz.map((p) => [p.id, p])); const havuzMap = new Map(havuz.map((p) => [p.id, p]));
const efektif = (id: string) => { const efektif = (id: string) => {
const p = havuzMap.get(id)!; const p = havuzMap.get(id)!;
return p.sira2025 ?? p.sira2024 ?? Number.MAX_SAFE_INTEGER; return p.efektifSira ?? Number.MAX_SAFE_INTEGER;
}; };
let secim: Record<Dilim, AiSecim[]> | null = null; let secim: Record<Dilim, AiSecim[]> | null = null;

View File

@@ -3,6 +3,7 @@
import path from "node:path"; import path from "node:path";
import Database from "better-sqlite3"; import Database from "better-sqlite3";
import { KATEGORILER, kategoriBul } from "./kategoriler"; import { KATEGORILER, kategoriBul } from "./kategoriler";
import { EFEKTIF_SIRA_SQL, SON_YIL, YILLAR } from "./veri-yillari";
import { import {
PUAN_TURLERI, PUAN_TURLERI,
type DilimKey, type DilimKey,
@@ -27,12 +28,46 @@ export function getDb(): Database.Database {
return globalForDb.yokatlasDb; return globalForDb.yokatlasDb;
} }
// 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).
export const SELECT_COLS = `id, isim, universite, unitur, il, fakulte, tur, sure, export const SELECT_COLS = `id, isim, universite, unitur, il, fakulte, tur, sure,
sira2025, sira2024, sira2023, sira2022, sira2021, ${YILLAR.map((yil) => `sira${yil} AS s${yil}`).join(", ")},
puan2025, kontenjan2025, yerlesen2025`; puan${SON_YIL} AS puanSon, kontenjan${SON_YIL} AS kontenjanSon, yerlesen${SON_YIL} AS yerlesenSon`;
// 2025 sıralaması yoksa (az yerleşen/yeni program) 2024'e düşer // Son yılın sıralaması yoksa (az yerleşen/yeni program) bir önceki yıla düşer.
export const EFEKTIF_SIRA = "COALESCE(sira2025, sira2024)"; // İ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. */
export type ProgramHamSatiri = Omit<
Program,
"siraGecmisi" | "efektifSira" | "siraSon"
> &
Record<`s${number}`, number | null>;
/** Ham DB satırını yılsız `Program`'a çevirir. */
export function programSatiri(row: ProgramHamSatiri): Program {
const siraGecmisi = YILLAR.map((yil) => row[`s${yil}`] ?? null);
const siraSon = siraGecmisi[siraGecmisi.length - 1] ?? null;
return {
id: row.id,
isim: row.isim,
universite: row.universite,
unitur: row.unitur,
il: row.il,
fakulte: row.fakulte,
tur: row.tur,
sure: row.sure,
siraGecmisi,
// EFEKTIF_SIRA ile aynı kural: son yıl, yoksa bir önceki yıl
efektifSira: siraSon ?? siraGecmisi[siraGecmisi.length - 2] ?? null,
siraSon,
puanSon: row.puanSon,
kontenjanSon: row.kontenjanSon,
yerlesenSon: row.yerlesenSon,
};
}
/** /**
* Kullanıcının başarı sıralamasına göre programları üç dilime ayırır. * Kullanıcının başarı sıralamasına göre programları üç dilime ayırır.
@@ -112,18 +147,18 @@ export function dilimAra(
// hayal: sınıra en yakın (en büyük taban) önce; diğerleri en yakından uzağa // hayal: sınıra en yakın (en büyük taban) önce; diğerleri en yakından uzağa
const yon = dilim === "hayal" ? "DESC" : "ASC"; const yon = dilim === "hayal" ? "DESC" : "ASC";
const programlar = db const satirlar = db
.prepare( .prepare(
`SELECT ${SELECT_COLS} ${base} ORDER BY ${EFEKTIF_SIRA} ${yon} LIMIT ? OFFSET ?` `SELECT ${SELECT_COLS} ${base} ORDER BY ${EFEKTIF_SIRA} ${yon} LIMIT ? OFFSET ?`
) )
.all(...args, limit, offset) as Program[]; .all(...args, limit, offset) as ProgramHamSatiri[];
const toplam = ( const toplam = (
db.prepare(`SELECT COUNT(*) AS adet ${base}`).get(...args) as { db.prepare(`SELECT COUNT(*) AS adet ${base}`).get(...args) as {
adet: number; adet: number;
} }
).adet; ).adet;
return { programlar, toplam }; return { programlar: satirlar.map(programSatiri), toplam };
} }
export function searchByRank( export function searchByRank(

View File

@@ -1,5 +1,11 @@
import "server-only"; import "server-only";
import { getDb, SELECT_COLS, EFEKTIF_SIRA } from "./db"; import {
getDb,
programSatiri,
SELECT_COLS,
EFEKTIF_SIRA,
type ProgramHamSatiri,
} from "./db";
import type { Program } from "@/types/yokatlas"; import type { Program } from "@/types/yokatlas";
import { kategoriBul, type Kategori } from "./kategoriler"; import { kategoriBul, type Kategori } from "./kategoriler";
import { uniAdiNormalize } from "./harita"; import { uniAdiNormalize } from "./harita";
@@ -24,7 +30,7 @@ export type BolumOzet = {
kategoriler: Kategori[]; kategoriler: Kategori[];
lisans: boolean; lisans: boolean;
onlisans: boolean; onlisans: boolean;
/** MIN(COALESCE(sira2025, sira2024)) — en iyi (en küçük) taban sıralaması */ /** MIN(efektif sıra) — en iyi (en küçük) taban sıralaması */
enIyiSira: number | null; enIyiSira: number | null;
}; };
@@ -131,13 +137,15 @@ export function getBolumBySlug(slug: string): BolumDetay | null {
if (!kayit) return null; if (!kayit) return null;
const placeholders = kayit.hamIsimler.map(() => "?").join(","); const placeholders = kayit.hamIsimler.map(() => "?").join(",");
const programlar = getDb() const programlar = (
getDb()
.prepare( .prepare(
`SELECT ${SELECT_COLS} FROM programs `SELECT ${SELECT_COLS} FROM programs
WHERE isim IN (${placeholders}) WHERE isim IN (${placeholders})
ORDER BY (${EFEKTIF_SIRA} IS NULL), ${EFEKTIF_SIRA} ASC`, ORDER BY (${EFEKTIF_SIRA} IS NULL), ${EFEKTIF_SIRA} ASC`,
) )
.all(...kayit.hamIsimler) as (Program & { onlisans?: number })[]; .all(...kayit.hamIsimler) as ProgramHamSatiri[]
).map(programSatiri);
// onlisans SELECT_COLS içinde yok; tur üzerinden ayır (TYT = önlisans) // onlisans SELECT_COLS içinde yok; tur üzerinden ayır (TYT = önlisans)
const lisansProgramlari = programlar.filter((p) => p.tur !== "TYT"); const lisansProgramlari = programlar.filter((p) => p.tur !== "TYT");
@@ -156,14 +164,14 @@ export function getBolumBySlug(slug: string): BolumDetay | null {
if (p.unitur === "DEVLET") devlet += 1; if (p.unitur === "DEVLET") devlet += 1;
else if (p.unitur) vakif += 1; else if (p.unitur) vakif += 1;
else bilinmeyenTur += 1; else bilinmeyenTur += 1;
const efektif = p.sira2025 ?? p.sira2024; const efektif = p.efektifSira;
if (efektif != null) { if (efektif != null) {
if (minSira == null || efektif < minSira) minSira = efektif; if (minSira == null || efektif < minSira) minSira = efektif;
if (maxSira == null || efektif > maxSira) maxSira = efektif; if (maxSira == null || efektif > maxSira) maxSira = efektif;
} }
if (p.puan2025 != null) { if (p.puanSon != null) {
if (minPuan == null || p.puan2025 < minPuan) minPuan = p.puan2025; if (minPuan == null || p.puanSon < minPuan) minPuan = p.puanSon;
if (maxPuan == null || p.puan2025 > maxPuan) maxPuan = p.puan2025; if (maxPuan == null || p.puanSon > maxPuan) maxPuan = p.puanSon;
} }
} }
@@ -357,13 +365,15 @@ export function getUniversiteBySlug(slug: string): UniDetay | null {
if (!kayit) return null; if (!kayit) return null;
const placeholders = kayit.hamAdlar.map(() => "?").join(","); const placeholders = kayit.hamAdlar.map(() => "?").join(",");
const programlar = getDb() const programlar = (
getDb()
.prepare( .prepare(
`SELECT ${SELECT_COLS} FROM programs `SELECT ${SELECT_COLS} FROM programs
WHERE universite IN (${placeholders}) WHERE universite IN (${placeholders})
ORDER BY fakulte, (${EFEKTIF_SIRA} IS NULL), ${EFEKTIF_SIRA} ASC`, ORDER BY fakulte, (${EFEKTIF_SIRA} IS NULL), ${EFEKTIF_SIRA} ASC`,
) )
.all(...kayit.hamAdlar) as Program[]; .all(...kayit.hamAdlar) as ProgramHamSatiri[]
).map(programSatiri);
const gruplar = new Map<string, Program[]>(); const gruplar = new Map<string, Program[]>();
let minSira: number | null = null; let minSira: number | null = null;
@@ -373,11 +383,11 @@ export function getUniversiteBySlug(slug: string): UniDetay | null {
const liste = gruplar.get(anahtar); const liste = gruplar.get(anahtar);
if (liste) liste.push(p); if (liste) liste.push(p);
else gruplar.set(anahtar, [p]); else gruplar.set(anahtar, [p]);
const efektif = p.sira2025 ?? p.sira2024; const efektif = p.efektifSira;
if (efektif != null && (minSira == null || efektif < minSira)) { if (efektif != null && (minSira == null || efektif < minSira)) {
minSira = efektif; minSira = efektif;
} }
toplamKontenjan += p.kontenjan2025 ?? 0; toplamKontenjan += p.kontenjanSon ?? 0;
} }
const ozet = getAllUniversiteler().find((u) => u.slug === slug)!; const ozet = getAllUniversiteler().find((u) => u.slug === slug)!;

View File

@@ -8,6 +8,7 @@ import {
} from "@/types/yokatlas"; } from "@/types/yokatlas";
import type { RaporParams } from "@/features/rapor/types/rapor"; import type { RaporParams } from "@/features/rapor/types/rapor";
import { kategoriEslesir } from "./kategoriler"; import { kategoriEslesir } from "./kategoriler";
import { SON_YIL, YILLAR } from "./veri-yillari";
export type Dilim = "hayal" | "dengeli" | "garanti"; export type Dilim = "hayal" | "dengeli" | "garanti";
@@ -155,6 +156,13 @@ export function havuzOlustur(params: RaporParams): HavuzSonuc {
return { havuz: dilimBasinaKirp(ilsizHam, PROMPT_LIMIT_DILIM), gevsetildi }; return { havuz: dilimBasinaKirp(ilsizHam, PROMPT_LIMIT_DILIM), gevsetildi };
} }
// Prompt'a veri penceresinin tamamı değil son 4 yılı gider (yeniden eskiye):
// pencere uzadıkça satır başına token sayısı sabit kalır.
const PROMPT_YIL_SAYISI = 4;
const PROMPT_YIL_INDEKSLERI = YILLAR.map((_, i) => i)
.slice(-PROMPT_YIL_SAYISI)
.reverse();
/** Prompt'a enjekte edilecek kompakt satır (token tasarrufu). */ /** Prompt'a enjekte edilecek kompakt satır (token tasarrufu). */
export function havuzuKompaktJson(havuz: AdayProgram[]): string { export function havuzuKompaktJson(havuz: AdayProgram[]): string {
return JSON.stringify( return JSON.stringify(
@@ -165,14 +173,16 @@ export function havuzuKompaktJson(havuz: AdayProgram[]): string {
unitur: p.unitur, unitur: p.unitur,
il: p.il, il: p.il,
dilim: p.dilim, dilim: p.dilim,
sira: { // Anahtarlar yıl taşır (y<yıl>, kontenjan<yıl>): model rakamın hangi
y2025: p.sira2025, // yıla ait olduğunu anahtardan okur.
y2024: p.sira2024, sira: Object.fromEntries(
y2023: p.sira2023, PROMPT_YIL_INDEKSLERI.map((i) => [
y2022: p.sira2022, `y${YILLAR[i]}`,
}, p.siraGecmisi[i] ?? null,
kontenjan2025: p.kontenjan2025, ]),
yerlesen2025: p.yerlesen2025, ),
[`kontenjan${SON_YIL}`]: p.kontenjanSon,
[`yerlesen${SON_YIL}`]: p.yerlesenSon,
})), })),
); );
} }

View File

@@ -44,7 +44,7 @@ export function raporMaskele(rapor: RaporSonuc): MaskeliRapor {
// kilitli metinler istemciye hiç ulaşmaz. // kilitli metinler istemciye hiç ulaşmaz.
uyarilar: rapor.uyarilar?.slice(0, 1) ?? [], uyarilar: rapor.uyarilar?.slice(0, 1) ?? [],
kilitliUyariSayisi: Math.max(0, (rapor.uyarilar?.length ?? 0) - 1), kilitliUyariSayisi: Math.max(0, (rapor.uyarilar?.length ?? 0) - 1),
// Program adı, üniversite ve 2021–2025 taban sıralamaları ham veridir. // Program adı, üniversite ve geçmiş yılların taban sıralamaları ham veridir.
programlar: rapor.programlar, programlar: rapor.programlar,
// Kapsam (kaç program bulundu / dağılım uyarlandı mı) dürüstlük bilgisi: // Kapsam (kaç program bulundu / dağılım uyarlandı mı) dürüstlük bilgisi:
// paketsiz kullanıcı da görür, maskelenmez. // paketsiz kullanıcı da görür, maskelenmez.

View File

@@ -32,9 +32,10 @@ export const RISK_PIN_RENK: Record<RiskSeviyesi, string> = {
}; };
/** /**
* Deterministik risk: programın efektif taban sıralaması (COALESCE 2025→2024) * Deterministik risk: programın efektif taban sıralaması (son yılın sırası,
* adayın sıralamasıyla kıyaslanır. Eşikler db.ts'teki dilim aralıklarıyla * yoksa bir önceki yılınki) adayın sıralamasıyla kıyaslanır. Eşikler
* birebir aynı (dengeli: [sira, 1.4×sira], garanti: (1.4×sira, ∞)). * db.ts'teki dilim aralıklarıyla birebir aynı (dengeli: [sira, 1.4×sira],
* garanti: (1.4×sira, ∞)).
*/ */
export function riskHesapla( export function riskHesapla(
efektifSira: number | null | undefined, efektifSira: number | null | undefined,
@@ -72,7 +73,8 @@ export function dilimdenFarkliRiskEtiketi(
* Risk seviyesinin tek cümlelik açıklaması (rapor satırları + tadımlık). * Risk seviyesinin tek cümlelik açıklaması (rapor satırları + tadımlık).
* Ürün yerleşme olasılığı HESAPLAMAZ; yalnızca son taban sıralamasını adayın * Ürün yerleşme olasılığı HESAPLAMAZ; yalnızca son taban sıralamasını adayın
* sırasıyla kıyaslar. Bu yüzden metin olasılık/ihtimal/"yerleşirsin" demez. * sırasıyla kıyaslar. Bu yüzden metin olasılık/ihtimal/"yerleşirsin" demez.
* "Geçen yıl" da denmez: efektif sıra 2025 yoksa 2024'e düşüyor. * "Geçen yıl" da denmez: efektif sıra, son yılın verisi yoksa bir önceki
* yılınkine düşüyor.
*/ */
export function riskNotuMetni(risk: RiskSeviyesi | null): string { export function riskNotuMetni(risk: RiskSeviyesi | null): string {
switch (risk) { switch (risk) {

View File

@@ -10,6 +10,7 @@ import type { Dilim } from "./rapor-havuzu";
import type { RaporSonuc } from "@/features/rapor/types/rapor"; import type { RaporSonuc } from "@/features/rapor/types/rapor";
import { dilimdenRisk, riskHesapla, riskNotuMetni } from "./risk"; import { dilimdenRisk, riskHesapla, riskNotuMetni } from "./risk";
import { trendOzetiMetni } from "./trend"; import { trendOzetiMetni } from "./trend";
import { VERI_YIL_SAYISI } from "./veri-yillari";
/** Sihirbaz kategorisi seçilmemiş kullanıcı için kova×tür başına fallback satır. */ /** Sihirbaz kategorisi seçilmemiş kullanıcı için kova×tür başına fallback satır. */
export const GENEL_KATEGORI = "genel"; export const GENEL_KATEGORI = "genel";
@@ -54,16 +55,41 @@ export type TadimlikTip = "genel" | "devlet" | "vakif";
* gerekce, satırın servis edilen TEK LLM metnidir (batch'te temsili sıraya * gerekce, satırın servis edilen TEK LLM metnidir (batch'te temsili sıraya
* göre yazıldı). Ürün yerleşme olasılığı hesaplamaz; bu yüzden olasılık / * göre yazıldı). Ürün yerleşme olasılığı hesaplamaz; bu yüzden olasılık /
* ihtimal / şans / garanti / kesin / yüzde / "yerleşirsin" diyen gerekçe * ihtimal / şans / garanti / kesin / yüzde / "yerleşirsin" diyen gerekçe
* servis edilmez. Eski batch'in "son 4 yıl" kalıbı da takılır (veri 5 yılı * servis edilmez. Kalıp küçük harfledir: metin önce Türkçe kurallarıyla
* kapsıyor, bkz. veri-kapsami.ts). Kalıp küçük harfledir: metin önce Türkçe * küçültülür ("İhtimal" → "ihtimal"; düz /i bayrağı İ/ı'yı eşlemez).
* kurallarıyla küçültülür ("İhtimal" → "ihtimal"; düz /i bayrağı İ/ı'yı
* eşlemez).
*/ */
const YASAK_GEREKCE_DILI = const YASAK_GEREKCE_DILI =
/olasılı|ihtimal|şans|garanti|kesin|yüzde|yerleşirsin|yerleşebil|son (4|dört) yıl/; /olasılı|ihtimal|şans|garanti|kesin|yüzde|yerleşirsin|yerleşebil/;
// Bayat batch kalıbı: gerekçe "son N yıl" diyorsa N, verinin bugün kapsadığı
// yıl sayısı (VERI_YIL_SAYISI) olmalı. Veri penceresi uzayınca eski batch'in
// "son 5 yıl"ı da, daha eskisinin "son 4 yıl"ı da kendiliğinden takılır.
const SON_N_YIL = /son (\d+|[a-zçğıöşü]+) yıl/g;
const SAYI_SOZCUKLERI: Record<string, number> = {
bir: 1,
iki: 2,
üç: 3,
dört: 4,
beş: 5,
altı: 6,
yedi: 7,
sekiz: 8,
dokuz: 9,
on: 10,
};
function bayatYilSayisiVar(kucukMetin: string): boolean {
for (const [, ham] of kucukMetin.matchAll(SON_N_YIL)) {
const n = /^\d+$/.test(ham) ? Number(ham) : SAYI_SOZCUKLERI[ham];
// Sayı olmayan sözcük ("son birkaç yıl") yıl sayısı iddiası değildir
if (n != null && n !== VERI_YIL_SAYISI) return true;
}
return false;
}
export function gerekceServisEdilebilir(gerekce: string): boolean { export function gerekceServisEdilebilir(gerekce: string): boolean {
return !YASAK_GEREKCE_DILI.test(gerekce.toLocaleLowerCase("tr-TR")); const kucuk = gerekce.toLocaleLowerCase("tr-TR");
return !YASAK_GEREKCE_DILI.test(kucuk) && !bayatYilSayisiVar(kucuk);
} }
/** /**
@@ -108,10 +134,12 @@ export async function tadimlikSec(
// aynı formülden adayın gerçek sırasına göre; trend, rapordakiyle aynı // aynı formülden adayın gerçek sırasına göre; trend, rapordakiyle aynı
// saf fonksiyondan programın sıra geçmişinden okuma anında üretilir — // saf fonksiyondan programın sıra geçmişinden okuma anında üretilir —
// metin rozetle çelişemez, "son 4 yıl" gibi bayat kalıp taşınmaz. // metin rozetle çelişemez, "son 4 yıl" gibi bayat kalıp taşınmaz.
// Snapshot'ın sıra dizisi üretildiği günün penceresidir (eski
// satırlarda 5 eleman); yıllar dizinin kendi çapasından okunur.
riskNotu: riskNotuMetni( riskNotu: riskNotuMetni(
riskHesapla(program.efektifSira, sira) ?? dilimdenRisk(satir.dilim), riskHesapla(program.efektifSira, sira) ?? dilimdenRisk(satir.dilim),
), ),
trendOzeti: trendOzetiMetni(program.siraGecmisi), trendOzeti: trendOzetiMetni(program.siraGecmisi, program.ilkYil),
program, program,
}; };
} }

View File

@@ -4,12 +4,7 @@
// düzeyine indi"); yıl aralığı sabit bir "son N yıl" kalıbından değil, // düzeyine indi"); yıl aralığı sabit bir "son N yıl" kalıbından değil,
// serideki dolu yıllardan gelir. // serideki dolu yıllardan gelir.
/** import { ILK_YIL } from "./veri-yillari";
* Sıra geçmişi dizisinin ilk elemanının yılı. Dizi her yerde 2021'den başlar
* (bkz. programBilgisi, raporSiraSerisi); yeni veri yılı sona eklenir, bu
* çapa değişmez.
*/
const ILK_VERI_YILI = 2021;
/** 45000 -> "45B", 1_200_000 -> "1,2M" (şemadaki öğrenci dili). */ /** 45000 -> "45B", 1_200_000 -> "1,2M" (şemadaki öğrenci dili). */
function siraKisa(n: number): string { function siraKisa(n: number): string {
@@ -21,7 +16,10 @@ function siraKisa(n: number): string {
} }
/** /**
* [2021..] taban sıralaması dizisinden tek cümlelik trend özeti. * [ilkYil..] taban sıralaması dizisinden tek cümlelik trend özeti. Dizi
* `ilkYil`'a demirlidir (varsayılan: veri penceresinin ilk yılı); yeni veri
* yılı sona eklenir, bu yüzden eski kayıtların daha kısa dizileri de doğru
* yılları verir. `ilkYil`, snapshot kendi çapasını taşıyorsa ondan gelir.
* *
* Cümle yalnızca OLANI betimler: ilk ve son dolu yılın tabanını kıyaslar * Cümle yalnızca OLANI betimler: ilk ve son dolu yılın tabanını kıyaslar
* (±%10 eşiği). "Zorlaşıyor / kolaylaşıyor" gibi geleceğe dönük yargı * (±%10 eşiği). "Zorlaşıyor / kolaylaşıyor" gibi geleceğe dönük yargı
@@ -30,22 +28,23 @@ function siraKisa(n: number): string {
*/ */
export function trendOzetiMetni( export function trendOzetiMetni(
gecmis: readonly (number | null | undefined)[] | undefined, gecmis: readonly (number | null | undefined)[] | undefined,
ilkYil: number = ILK_YIL,
): string { ): string {
const seri: Array<[number, number]> = []; const seri: Array<[number, number]> = [];
(gecmis ?? []).forEach((sira, i) => { (gecmis ?? []).forEach((sira, i) => {
if (sira != null) seri.push([ILK_VERI_YILI + i, sira]); if (sira != null) seri.push([ilkYil + i, sira]);
}); });
if (seri.length < 2) { if (seri.length < 2) {
return "Karşılaştırma için yeterli geçmiş yıl verisi yok; trendi temkinli oku."; return "Karşılaştırma için yeterli geçmiş yıl verisi yok; trendi temkinli oku.";
} }
const [ilkYil, ilkSira] = seri[0]; const [basYil, ilkSira] = seri[0];
const [sonYil, sonSira] = seri[seri.length - 1]; const [sonYil, sonSira] = seri[seri.length - 1];
const oran = (sonSira - ilkSira) / ilkSira; const oran = (sonSira - ilkSira) / ilkSira;
if (oran <= -0.1) { if (oran <= -0.1) {
return `${ilkYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine indi; taban sıkılaştı.`; return `${basYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine indi; taban sıkılaştı.`;
} }
if (oran >= 0.1) { if (oran >= 0.1) {
return `${ilkYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine çıktı; taban gevşedi.`; return `${basYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine çıktı; taban gevşedi.`;
} }
return `${ilkYil}-${sonYil} arasında taban sıralaması ${siraKisa(sonSira)} düzeyinde stabil seyretti.`; return `${basYil}-${sonYil} arasında taban sıralaması ${siraKisa(sonSira)} düzeyinde stabil seyretti.`;
} }

View File

@@ -1,6 +1,6 @@
/** /**
* Taban sıralaması geçmişinin kapsadığı yıl sayısı. Veritabanında * Taban sıralaması geçmişinin kapsadığı yıl sayısı; "son N yıl" diyen tüm
* sira2021–sira2025 kolonları var (5 yıl); "son N yıl" diyen tüm metinler bu * metinler bu sabitten okur. Değer yıl kapsamının tek kaynağından
* sabitten okur. Yeni veri yılı eklenince (ör. sira2026) burayı güncelle. * (lib/veri-yillari.ts) türetilir — yeni veri yılı eklenince burası değişmez.
*/ */
export const VERI_YIL_SAYISI = 5; export { VERI_YIL_SAYISI } from "./veri-yillari";

View File

@@ -11,14 +11,16 @@ export type Program = {
fakulte: string | null; fakulte: string | null;
tur: string; tur: string;
sure: number | null; sure: number | null;
sira2025: number | null; /** Taban sıralamaları; `YILLAR` (lib/veri-yillari) ile indeks hizalı. */
sira2024: number | null; siraGecmisi: (number | null)[];
sira2023: number | null; /** Son yılın sırası; o yıl verisi yoksa bir önceki yılınki. */
sira2022: number | null; efektifSira: number | null;
sira2021: number | null; // "Son" = en son yerleştirme yılı (SON_YIL). Alan adları bilinçli yılsız:
puan2025: number | null; // yeni veri yılı yüklenince tip ve tüketiciler değişmez.
kontenjan2025: number | null; siraSon: number | null;
yerlesen2025: number | null; puanSon: number | null;
kontenjanSon: number | null;
yerlesenSon: number | null;
}; };
export const PUAN_TURLERI = { export const PUAN_TURLERI = {
@@ -42,11 +44,15 @@ export type RankResults = {
}; };
/** /**
* /sonuc ham tablosunun satırı: tablo `fakulte` ve `puan2025`'i hiç * /sonuc ham tablosunun satırı: tablo `fakulte`, `puanSon` ve `siraSon`'u
* (dizinin son elemanı; tablo `efektifSira` + `siraGecmisi` ile çalışır) hiç
* kullanmadığı için bu alanlar client'a serileştirilmez (satır başına en * kullanmadığı için bu alanlar client'a serileştirilmez (satır başına en
* uzun string alanlardan biri fakulte). Katalog sayfaları tam Program kullanır. * uzun string alanlardan biri fakulte). Katalog sayfaları tam Program kullanır.
*/ */
export type ProgramListeSatiri = Omit<Program, "fakulte" | "puan2025">; export type ProgramListeSatiri = Omit<
Program,
"fakulte" | "puanSon" | "siraSon"
>;
export type RankResultsListe = { export type RankResultsListe = {
hayal: ProgramListeSatiri[]; hayal: ProgramListeSatiri[];