Merge branch 'gece/p3b-tadimlik-dili' into gece/entegrasyon-test-2

This commit is contained in:
bilalgursen
2026-09-21 09:27:12 +03:00
12 changed files with 151 additions and 67 deletions

View File

@@ -67,9 +67,10 @@ KURALLAR (kesin):
- METİN DİYETİ: her alan TEK kısa cümle, dolgu söz yok.
- gerekce: bu programın bu aday için neden mantıklı olduğu (TEK cümle,
en fazla ~15 kelime, Türkçe).
- riskNotu: yerleşme ihtimali değerlendirmesi (TEK kısa cümle). ASLA "garanti",
"kesin", "yüzde yüz" deme; "yüksek/orta/düşük ihtimal" dili kullan.
- trendOzeti: son 4 yılın taban sıralaması hareketi, TEK cümle (ör. "2022'den
- riskNotu: geçmiş taban verisine dayalı risk değerlendirmesi (TEK kısa cümle).
ASLA garanti/kesin/yüzde/olasılık/ihtimal deme; "güvenli / az riskli /
riskli" etiketleriyle konuş.
- trendOzeti: son 5 yılın taban sıralaması hareketi, TEK cümle (ör. "2022'den
beri taban 45B'den 38B'ye indi, bölüm zorlaşıyor").
- Adayın sıralaması yaklaşıktır; kesin sıra telaffuz etme, banda göre konuş.`;
@@ -82,9 +83,9 @@ function mockMetin(p: Program, temsiliSira: number, kategori: string | null) {
return {
gerekce: `(Dev mock) ${p.isim}, ${kategori ? `${kategori} ilgisiyle ve ` : ""}~${temsiliSira.toLocaleString("tr-TR")} bandındaki sıralamanla uyumlu bir aday.`,
riskNotu:
"(Dev mock) Taban sıralaması senin civarında; orta-yüksek ihtimalle yerleşilebilir.",
"(Dev mock) Son taban sıralaması sırana yakın; küçük bir kayma sonucu değiştirebilir.",
trendOzeti:
"(Dev mock) Son 4 yılda taban sıralaması görece stabil seyretti.",
"(Dev mock) Son 5 yılda taban sıralaması görece stabil seyretti.",
};
}

View File

@@ -45,7 +45,8 @@ KURALLAR:
sıralaması adayın belirgin altında, Az riskli (sarı) = adayın sıralaması
civarında, Riskli (kırmızı) = geçen yıl adayın sıralamasının üzerinde
kapandı. Risk sorularında bu etiketleri ve taban sıralarını kullan.
- ASLA yerleşme garantisi verme; "yüksek/orta/düşük ihtimal" dili kullan.
- ASLA yerleşme garantisi verme; olasılık, yüzde ya da "ihtimal" dili kullanma —
güvenli / az riskli / riskli etiketleri ve taban sıralarıyla konuş.
- Bilmediğin güncel veriyi uydurma; emin değilsen YÖK Atlas'a bakmasını söyle.
- Taban sıralamalarının her yıl değiştiğini gerektiğinde hatırlat.
- Cevapları kısa ve öz tut (genelde 3-6 cümle); istenirse detaylandır.

View File

@@ -52,7 +52,7 @@ export const metadata: Metadata = {
template: "%s — KolayTercih",
},
description:
"YKS sıralamana göre gerçek YÖK Atlas verisiyle dengeli 24 tercihlik liste. İnsan danışmanın onda bir fiyatına, yapay zekâ destekli tercih danışmanlığı.",
"YKS sıralamana göre gerçek YÖK Atlas verisiyle dengeli 24 tercihlik liste. İnsan danışmandan çok daha düşük fiyata, yapay zekâ destekli tercih danışmanlığı.",
applicationName: "KolayTercih",
openGraph: {
type: "website",

View File

@@ -30,6 +30,7 @@ import {
SectionEyebrow,
} from "@/components/pixel-decor";
import { MAX_REVIZYON, URUNLER } from "@/lib/credits";
import { VERI_YIL_SAYISI } from "@/lib/veri-kapsami";
import {
Accordion,
AccordionContent,
@@ -87,7 +88,7 @@ const problems = [
icon: Wallet,
title: "Danışman lüks haline geldi",
description:
"İyi bir tercih danışmanı 2.000–10.000 TL istiyor. Ücretsiz robotlar ise sadece liste filtreliyor, akıl vermiyor.",
"İyi bir tercih danışmanının ücreti birkaç bin lirayı bulabiliyor. Ücretsiz robotlar ise sadece liste filtreliyor, akıl vermiyor.",
},
];
@@ -103,8 +104,7 @@ const steps = [
icon: BarChart3,
step: "2",
title: "Yapay zekâ veriyi tarasın",
description:
"Son 4 yılın YÖK Atlas verisi — taban sıralaması trendleri, kontenjan değişimleri, doluluk oranları — senin için analiz edilsin.",
description: `Son ${VERI_YIL_SAYISI} yılın YÖK Atlas verisi — taban sıralaması trendleri, kontenjan değişimleri, doluluk oranları — senin için analiz edilsin.`,
},
{
icon: ShieldCheck,
@@ -118,8 +118,7 @@ const steps = [
const faqs = [
{
question: "KolayTercih'in önerileri neye dayanıyor?",
answer:
"Tüm öneriler YÖK Atlas'ın resmî verisine dayanır: son 4 yılın taban başarı sıralamaları, kontenjanlar, yerleşme istatistikleri ve doluluk oranları. Yapay zekâ bu veriyi yorumlar; veri olmadan tahmin yürütmez.",
answer: `Tüm öneriler YÖK Atlas'ın resmî verisine dayanır: son ${VERI_YIL_SAYISI} yılın taban başarı sıralamaları, kontenjanlar, yerleşme istatistikleri ve doluluk oranları. Yapay zekâ bu veriyi yorumlar; veri olmadan tahmin yürütmez.`,
},
{
question: "Ücretsiz tercih robotlarından farkı ne?",
@@ -145,7 +144,7 @@ const comparison = [
tercihai: true,
},
{
feature: "Son 4 yılın trend analizi",
feature: `Son ${VERI_YIL_SAYISI} yılın trend analizi`,
robot: false,
danisman: true,
tercihai: true,
@@ -165,7 +164,7 @@ const comparison = [
{
feature: "Fiyat",
robot: "Ücretsiz",
danisman: "2.000–10.000 TL",
danisman: "Danışmana göre değişir",
tercihai: `${PAKET_FIYATI} TL · tek seferlik`,
},
];
@@ -177,7 +176,7 @@ const paketOzellikleri = [
},
{
icon: ShieldCheck,
text: "Her tercih için yerleşme olasılığı ve risk analizi (son 4 yıl)",
text: `Her tercih için yerleşme riski ve trend analizi (son ${VERI_YIL_SAYISI} yıl)`,
},
{ icon: FileText, text: "Veliyle paylaşılabilir PDF rapor" },
{ icon: RefreshCcw, text: `${MAX_REVIZYON} liste revizyon hakkı` },
@@ -362,7 +361,7 @@ export default function Home() {
{[
{ value: "23.000+", label: "Taranan lisans programı" },
{
value: "4 yıl",
value: `${VERI_YIL_SAYISI} yıl`,
label: "Taban sıralaması trend verisi",
},
{ value: "%100", label: "Resmî YÖK Atlas kaynağı" },
@@ -481,7 +480,7 @@ export default function Home() {
Ne kadar? Ne dahil?
</h2>
<p className="max-w-xl text-lg leading-relaxed text-slate-600">
İnsan danışmanın 2.000–10.000 TL aldığı işi tek seferlik{" "}
İnsan danışmanın birkaç bin liraya yaptığı işi tek seferlik{" "}
{PAKET_FIYATI} TL&apos;ye alırsın. Abonelik yok, gizli ücret
yok.
</p>

View File

@@ -46,11 +46,12 @@ export default function PaketPage({
<SectionEyebrow>Tek seferlik ödeme · Abonelik yok</SectionEyebrow>
<h1 className="mt-4 font-heading text-3xl font-bold sm:text-4xl">
İnsan danışmanın işini,{" "}
<span className="text-primary">onda bir fiyatına</span>
<span className="text-primary">çok daha düşük fiyata</span>
</h1>
<p className="mx-auto mt-3 max-w-xl text-slate-600">
Tercih danışmanları bu dönemde 2.000–10.000 TL alıyor. KolayTercih
aynı işi gerçek YÖK Atlas verisiyle, dakikalar içinde yapar.
Tercih danışmanlarının ücreti bu dönemde birkaç bin lirayı
bulabiliyor. KolayTercih aynı işi gerçek YÖK Atlas verisiyle,
dakikalar içinde yapar.
</p>
</Parallax>

View File

@@ -35,7 +35,7 @@ const FUNNEL_METINLERI = [
"Sıralaman ne söylüyor? Birlikte bakalım.",
"Her satırı senin için hesaplıyoruz.",
"Akıllı tercih, içi rahat bir bekleyiş.",
"Yerleşme ihtimalini görerek seç.",
"Yerleşme riskini görerek seç.",
"Sana özel liste birkaç dakikada hazır.",
"İlgi alanların ve şehrin, tek listede.",
"Boş kalan tercih, kaçan fırsattır.",

View File

@@ -12,6 +12,7 @@ import {
} from "lucide-react";
import { getCurrentUser } from "@/lib/session";
import { URUNLER, DENEME_KREDISI } from "@/lib/credits";
import { VERI_YIL_SAYISI } from "@/lib/veri-kapsami";
import { Button } from "@/components/ui/button";
import { Parallax } from "@/components/parallax";
import { Skeleton } from "@/components/ui/skeleton";
@@ -28,7 +29,7 @@ const OZELLIKLER = [
},
{
icon: ShieldCheck,
text: "Her tercih için yerleşme olasılığı ve risk analizi (son 4 yıl trend)",
text: `Her tercih için yerleşme riski ve trend analizi (son ${VERI_YIL_SAYISI} yıl)`,
},
{ icon: FileText, text: "Veliyle paylaşılabilir PDF rapor" },
{ icon: RefreshCcw, text: "2 liste revizyon hakkı" },

View File

@@ -18,7 +18,13 @@ import {
type AdayProgram,
} from "../rapor-havuzu";
import { secimOzeti } from "@/features/sihirbaz/sihirbaz-sabitler";
import { RISK_ETIKET, dilimdenRisk, riskHesapla } from "../risk";
import {
RISK_ETIKET,
dilimdenRisk,
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
@@ -127,50 +133,19 @@ 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 {
switch (riskHesapla(p.sira2025 ?? p.sira2024, adaySira)) {
case "guvenli":
return "Taban sıralaması senin sıranın belirgin altında; yüksek ihtimalle yerleşirsin.";
case "az-riskli":
return "Taban sıralaması sırana yakın; orta ihtimalle yerleşilebilir.";
case "riskli":
return "Taban sıralaması senin sıranın üstünde kapandı; yerleşme ihtimali düşük.";
default:
return "Güncel taban sıralaması verisi yok; ihtimal öngörülemez, temkinli yaklaş.";
}
return riskNotuMetni(riskHesapla(p.sira2025 ?? p.sira2024, adaySira));
}
/** Havuz programını render için gereken bilgiye indirger. */

View File

@@ -42,3 +42,22 @@ export function dilimdenRisk(dilim: Dilim | string): RiskSeviyesi {
if (dilim === "dengeli") return "az-riskli";
return "riskli";
}
/**
* 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
* 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.
*/
export function riskNotuMetni(risk: RiskSeviyesi | null): string {
switch (risk) {
case "guvenli":
return "Son taban sıralaması senin sıranın belirgin altında; geçmiş veriye göre güvenli aralıkta.";
case "az-riskli":
return "Son taban sıralaması sırana yakın; küçük bir kayma sonucu değiştirebilir.";
case "riskli":
return "Son taban sıralaması senin sıranın üstünde kapandı; hayal tercihi olarak düşün.";
default:
return "Güncel taban verisi yok; bu satırı temkinli değerlendir.";
}
}

View File

@@ -8,6 +8,8 @@ import { turkishSlugify } from "./slug";
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";
@@ -48,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
@@ -55,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,
@@ -75,14 +96,23 @@ 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,
riskNotu: satir.riskNotu,
trendOzeti: satir.trendOzeti,
program: satir.program as RaporSonuc["programlar"][string],
// 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: trendOzetiMetni(program.siraGecmisi),
program,
};
}
}

51
src/lib/trend.ts Normal file
View File

@@ -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.`;
}

6
src/lib/veri-kapsami.ts Normal file
View File

@@ -0,0 +1,6 @@
/**
* Taban sıralaması geçmişinin kapsadığı yıl sayısı. Veritabanında
* sira2021–sira2025 kolonları var (5 yıl); "son N yıl" diyen tüm metinler bu
* sabitten okur. Yeni veri yılı eklenince (ör. sira2026) burayı güncelle.
*/
export const VERI_YIL_SAYISI = 5;