From 79bc7fa8be41efd56a76cc8af92629a3e4876939 Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Mon, 21 Sep 2026 09:22:40 +0300 Subject: [PATCH] =?UTF-8?q?fix(ai):=20tad=C4=B1ml=C4=B1k=20trendi=20determ?= =?UTF-8?q?inistik=20+=20gerek=C3=A7e=20dil=20filtresi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cae2aaa yalnız riskNotu'yu deterministik yapmıştı; aynı satırın gerekce ve trendOzeti alanları DB'deki eski LLM metni olarak servis ediliyordu (lokal havuzda 89 satır "Son 4 yıl…", 13+ satır "yerleşme olasılığı/ihtimali"). Oradaki "olasılık dili sızamaz" yorumu yanlıştı; düzeltildi. - yeni src/lib/trend.ts: trendOzetiMetni() saf fonksiyonu — rapor.ts'teki trendOzetiUret gövdesi buraya taşındı, rapor ve tadımlık aynı kaynaktan okur. Yıl aralığı serideki dolu yıllardan gelir ("2021-2025 arasında…") - trend cümlesinden tahmin yargısı çıktı: "bölüm zorlaşıyor" → "taban sıkılaştı", "yerleşmek görece kolaylaşıyor" → "taban gevşedi". ±%10 eşiği, ilk–son yıl mantığı, 5/13/6 ve LLM şeması/prompt'u aynı - tadimlikSec(): DB'deki trendOzeti servis edilmez; program.siraGecmisi'nden okuma anında üretilir (alan yoksa "yeterli geçmiş yıl verisi yok") - tadimlikSec(): gerekce olasılık/ihtimal/şans/garanti/kesin/yüzde/ "yerleşirsin"/"son 4 yıl" içeriyorsa satır yok sayılır → "genel" kategoriye düşer, o da takılırsa null (tadımlık gizlenir). Eşleşme Türkçe küçük harfle yapılır (İ/ı) DB satırları, şema ve script'ler değişmedi. Co-Authored-By: Claude Fable 5.1 --- src/lib/ai/rapor.ts | 39 +++++++---------------------- src/lib/tadimlik-havuzu.ts | 34 ++++++++++++++++++++----- src/lib/trend.ts | 51 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 36 deletions(-) create mode 100644 src/lib/trend.ts diff --git a/src/lib/ai/rapor.ts b/src/lib/ai/rapor.ts index 3141b23..416db7a 100644 --- a/src/lib/ai/rapor.ts +++ b/src/lib/ai/rapor.ts @@ -24,6 +24,7 @@ import { riskHesapla, riskNotuMetni, } from "../risk"; +import { trendOzetiMetni } from "../trend"; // 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 @@ -132,37 +133,15 @@ function normalizeEt( // geçmişinden hesaplanır; model bu alanlarda jenerik dolgu yazıyordu, kod // kesin rakam verir ("45B düzeyinden 38B düzeyine indi"). -/** 45000 -> "45B", 1_200_000 -> "1,2M" (şemadaki öğrenci dili). */ -function siraKisa(n: number): string { - if (n >= 1_000_000) { - return `${(n / 1_000_000).toLocaleString("tr-TR", { maximumFractionDigits: 1 })}M`; - } - if (n >= 1000) return `${Math.round(n / 1000)}B`; - return String(n); -} - function trendOzetiUret(p: AdayProgram): string { - const ham: Array<[number, number | null | undefined]> = [ - [2021, p.sira2021], - [2022, p.sira2022], - [2023, p.sira2023], - [2024, p.sira2024], - [2025, p.sira2025], - ]; - const seri = ham.filter((cift): cift is [number, number] => cift[1] != null); - if (seri.length < 2) { - return "Karşılaştırma için yeterli geçmiş yıl verisi yok; trendi temkinli oku."; - } - const [ilkYil, ilkSira] = seri[0]; - const [sonYil, sonSira] = seri[seri.length - 1]; - const oran = (sonSira - ilkSira) / ilkSira; - if (oran <= -0.1) { - return `${ilkYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine indi; bölüm zorlaşıyor.`; - } - if (oran >= 0.1) { - return `${ilkYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine çıktı; yerleşmek görece kolaylaşıyor.`; - } - return `${ilkYil}-${sonYil} arasında taban sıralaması ${siraKisa(sonSira)} düzeyinde stabil seyretti.`; + // Cümle tadımlıkla ortak saf fonksiyondan gelir (lib/trend.ts) + return trendOzetiMetni([ + p.sira2021, + p.sira2022, + p.sira2023, + p.sira2024, + p.sira2025, + ]); } function riskNotuUret(p: AdayProgram, adaySira: number): string { diff --git a/src/lib/tadimlik-havuzu.ts b/src/lib/tadimlik-havuzu.ts index e1df7ae..7b01b7b 100644 --- a/src/lib/tadimlik-havuzu.ts +++ b/src/lib/tadimlik-havuzu.ts @@ -9,6 +9,7 @@ import type { PuanTuruKey } from "@/types/yokatlas"; import type { Dilim } from "./rapor-havuzu"; import type { RaporSonuc } from "@/features/rapor/types/rapor"; import { dilimdenRisk, riskHesapla, riskNotuMetni } from "./risk"; +import { trendOzetiMetni } from "./trend"; /** Sihirbaz kategorisi seçilmemiş kullanıcı için kova×tür başına fallback satır. */ export const GENEL_KATEGORI = "genel"; @@ -49,6 +50,22 @@ export interface TadimlikSatir { export type TadimlikTip = "genel" | "devlet" | "vakif"; +/** + * 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 / + * 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ı + * kapsıyor, bkz. veri-kapsami.ts). Kalıp küçük harfledir: metin önce Türkçe + * kurallarıyla küçültülür ("İhtimal" → "ihtimal"; düz /i bayrağı İ/ı'yı + * eşlemez). + */ +const YASAK_GEREKCE_DILI = + /olasılı|ihtimal|şans|garanti|kesin|yüzde|yerleşirsin|yerleşebil|son (4|dört) yıl/; + +export function gerekceServisEdilebilir(gerekce: string): boolean { + return !YASAK_GEREKCE_DILI.test(gerekce.toLocaleLowerCase("tr-TR")); +} + /** * Kova × tür × kategori × tip anahtarıyla tadımlık satırı seçer. Kategoriye * özel satır yoksa "genel" kategoriye düşer; o da yoksa null — çağıran taraf @@ -56,6 +73,9 @@ export type TadimlikTip = "genel" | "devlet" | "vakif"; * * Tip fallback'i YOKTUR: kullanıcı devlet/vakıf seçtiyse yanlış tipte örnek * asla gösterilmez — uygun satır yoksa tadımlık gizlenir. + * + * Gerekçesi dil filtresine takılan satır YOK sayılır: aynı sırayla "genel" + * kategoriye düşülür, o da takılırsa null (tadımlık gizlenir). */ export async function tadimlikSec( sira: number, @@ -76,20 +96,22 @@ export async function tadimlikSec( eq(schema.tadimlikHavuzu.tip, tip), ), }); - if (satir) { + if (satir && gerekceServisEdilebilir(satir.gerekce)) { const program = satir.program as RaporSonuc["programlar"][string]; return { programId: satir.programId, dilim: satir.dilim, + // Servis edilen tek LLM metni; yukarıdaki dil filtresinden geçti. gerekce: satir.gerekce, - // DB'deki riskNotu batch'te LLM'in temsili sıraya göre yazdığı - // cümledir; servis EDİLMEZ. Not, rozetle (tadimlik-satiri.tsx) aynı - // formülden adayın gerçek sırasına göre okuma anında üretilir — metin - // ile rozet çelişemez, olasılık dili sızamaz. + // DB'deki riskNotu ve trendOzeti batch'te LLM'in yazdığı cümlelerdir; + // ikisi de servis EDİLMEZ. Risk notu, rozetle (tadimlik-satiri.tsx) + // 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 — + // metin rozetle çelişemez, "son 4 yıl" gibi bayat kalıp taşınmaz. riskNotu: riskNotuMetni( riskHesapla(program.efektifSira, sira) ?? dilimdenRisk(satir.dilim), ), - trendOzeti: satir.trendOzeti, + trendOzeti: trendOzetiMetni(program.siraGecmisi), program, }; } diff --git a/src/lib/trend.ts b/src/lib/trend.ts new file mode 100644 index 0000000..4e3c8bd --- /dev/null +++ b/src/lib/trend.ts @@ -0,0 +1,51 @@ +// Deterministik trend cümlesi: rapor satırları (lib/ai/rapor.ts) ve girişsiz +// tadımlık (lib/tadimlik-havuzu.ts) AYNI saf fonksiyondan okur. Cümle gerçek +// YÖK Atlas sıra geçmişinden kesin rakamla kurulur ("45B düzeyinden 38B +// düzeyine indi"); yıl aralığı sabit bir "son N yıl" kalıbından değil, +// serideki dolu yıllardan gelir. + +/** + * 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). */ +function siraKisa(n: number): string { + if (n >= 1_000_000) { + return `${(n / 1_000_000).toLocaleString("tr-TR", { maximumFractionDigits: 1 })}M`; + } + if (n >= 1000) return `${Math.round(n / 1000)}B`; + return String(n); +} + +/** + * [2021..] taban sıralaması dizisinden tek cümlelik trend özeti. + * + * 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ı + * kurmaz — iki uç yılın kıyası aradaki seyri ve gelecek yılı söylemez. + * Küçülen sıra = taban sıkılaştı, büyüyen sıra = taban gevşedi. + */ +export function trendOzetiMetni( + gecmis: readonly (number | null | undefined)[] | undefined, +): string { + const seri: Array<[number, number]> = []; + (gecmis ?? []).forEach((sira, i) => { + if (sira != null) seri.push([ILK_VERI_YILI + i, sira]); + }); + if (seri.length < 2) { + return "Karşılaştırma için yeterli geçmiş yıl verisi yok; trendi temkinli oku."; + } + const [ilkYil, ilkSira] = seri[0]; + const [sonYil, sonSira] = seri[seri.length - 1]; + const oran = (sonSira - ilkSira) / ilkSira; + if (oran <= -0.1) { + return `${ilkYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine indi; taban sıkılaştı.`; + } + if (oran >= 0.1) { + return `${ilkYil}-${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.`; +}