Compare commits

...

2 Commits

Author SHA1 Message Date
bilalgursen
004473a078 chore(metin): danışman fiyat kıyası rakamsız ve atıfsız [Bilal onayı gerekir]
52520ac'nin ALTERNATİFİ (ikisi birlikte alınmaz). Canlıdaki "2.000–10.000
TL / onda bir" de, 52520ac'deki "1.000–5.000 TL — Armut, 2026" da teyitsiz;
bu commit TL aralığını, üçüncü taraf atfını ve "onda bir" oranını kaldırır:
- page.tsx sorun kartı: "…danışmanının ücreti birkaç bin lirayı bulabiliyor."
- page.tsx kıyas tablosu Fiyat/İnsan danışman: "Danışmana göre değişir"
- page.tsx fiyat bölümü: "İnsan danışmanın birkaç bin liraya yaptığı işi…"
- paket/page.tsx başlık: "onda bir fiyatına" → "çok daha düşük fiyata";
  alt metin: "…ücreti bu dönemde birkaç bin lirayı bulabiliyor."
- layout.tsx description: "İnsan danışmandan çok daha düşük fiyata, …"
Hero'daki yorum içi ölü <p> bloğuna dokunulmadı (9d450c5 ile çakışmasın).
Reddedilirse: git merge gece/p3b-tadimlik-dili~1

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 09:23:25 +03:00
bilalgursen
79bc7fa8be fix(ai): tadımlık trendi deterministik + gerekçe dil filtresi
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 <noreply@anthropic.com>
2026-09-21 09:22:40 +03:00
6 changed files with 96 additions and 43 deletions

View File

@@ -52,7 +52,7 @@ export const metadata: Metadata = {
template: "%s — KolayTercih", template: "%s — KolayTercih",
}, },
description: 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", applicationName: "KolayTercih",
openGraph: { openGraph: {
type: "website", type: "website",

View File

@@ -88,7 +88,7 @@ const problems = [
icon: Wallet, icon: Wallet,
title: "Danışman lüks haline geldi", title: "Danışman lüks haline geldi",
description: 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.",
}, },
]; ];
@@ -164,7 +164,7 @@ const comparison = [
{ {
feature: "Fiyat", feature: "Fiyat",
robot: "Ücretsiz", robot: "Ücretsiz",
danisman: "2.000–10.000 TL", danisman: "Danışmana göre değişir",
tercihai: `${PAKET_FIYATI} TL · tek seferlik`, tercihai: `${PAKET_FIYATI} TL · tek seferlik`,
}, },
]; ];
@@ -480,7 +480,7 @@ export default function Home() {
Ne kadar? Ne dahil? Ne kadar? Ne dahil?
</h2> </h2>
<p className="max-w-xl text-lg leading-relaxed text-slate-600"> <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 {PAKET_FIYATI} TL&apos;ye alırsın. Abonelik yok, gizli ücret
yok. yok.
</p> </p>

View File

@@ -46,11 +46,12 @@ export default function PaketPage({
<SectionEyebrow>Tek seferlik ödeme · Abonelik yok</SectionEyebrow> <SectionEyebrow>Tek seferlik ödeme · Abonelik yok</SectionEyebrow>
<h1 className="mt-4 font-heading text-3xl font-bold sm:text-4xl"> <h1 className="mt-4 font-heading text-3xl font-bold sm:text-4xl">
İnsan danışmanın işini,{" "} İ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> </h1>
<p className="mx-auto mt-3 max-w-xl text-slate-600"> <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 Tercih danışmanlarının ücreti bu dönemde birkaç bin lirayı
aynı işi gerçek YÖK Atlas verisiyle, dakikalar içinde yapar. bulabiliyor. KolayTercih aynı işi gerçek YÖK Atlas verisiyle,
dakikalar içinde yapar.
</p> </p>
</Parallax> </Parallax>

View File

@@ -24,6 +24,7 @@ import {
riskHesapla, riskHesapla,
riskNotuMetni, riskNotuMetni,
} from "../risk"; } from "../risk";
import { trendOzetiMetni } from "../trend";
// 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
@@ -132,37 +133,15 @@ function normalizeEt(
// geçmişinden hesaplanır; model bu alanlarda jenerik dolgu yazıyordu, kod // geçmişinden hesaplanır; model bu alanlarda jenerik dolgu yazıyordu, kod
// kesin rakam verir ("45B düzeyinden 38B düzeyine indi"). // 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 { function trendOzetiUret(p: AdayProgram): string {
const ham: Array<[number, number | null | undefined]> = [ // Cümle tadımlıkla ortak saf fonksiyondan gelir (lib/trend.ts)
[2021, p.sira2021], return trendOzetiMetni([
[2022, p.sira2022], p.sira2021,
[2023, p.sira2023], p.sira2022,
[2024, p.sira2024], p.sira2023,
[2025, p.sira2025], p.sira2024,
]; 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.`;
} }
function riskNotuUret(p: AdayProgram, adaySira: number): string { function riskNotuUret(p: AdayProgram, adaySira: number): string {

View File

@@ -9,6 +9,7 @@ import type { PuanTuruKey } from "@/types/yokatlas";
import type { Dilim } from "./rapor-havuzu"; 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";
/** 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";
@@ -49,6 +50,22 @@ export interface TadimlikSatir {
export type TadimlikTip = "genel" | "devlet" | "vakif"; 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 * 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 * ö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 * 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. * 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( export async function tadimlikSec(
sira: number, sira: number,
@@ -76,20 +96,22 @@ export async function tadimlikSec(
eq(schema.tadimlikHavuzu.tip, tip), eq(schema.tadimlikHavuzu.tip, tip),
), ),
}); });
if (satir) { if (satir && gerekceServisEdilebilir(satir.gerekce)) {
const program = satir.program as RaporSonuc["programlar"][string]; const program = satir.program as RaporSonuc["programlar"][string];
return { return {
programId: satir.programId, programId: satir.programId,
dilim: satir.dilim, dilim: satir.dilim,
// Servis edilen tek LLM metni; yukarıdaki dil filtresinden geçti.
gerekce: satir.gerekce, gerekce: satir.gerekce,
// DB'deki riskNotu batch'te LLM'in temsili sıraya göre yazdığı // DB'deki riskNotu ve trendOzeti batch'te LLM'in yazdığı cümlelerdir;
// cümledir; servis EDİLMEZ. Not, rozetle (tadimlik-satiri.tsx) aynı // ikisi de servis EDİLMEZ. Risk notu, rozetle (tadimlik-satiri.tsx)
// formülden adayın gerçek sırasına göre okuma anında üretilir — metin // aynı formülden adayın gerçek sırasına göre; trend, rapordakiyle aynı
// ile rozet çelişemez, olasılık dili sızamaz. // 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( riskNotu: riskNotuMetni(
riskHesapla(program.efektifSira, sira) ?? dilimdenRisk(satir.dilim), riskHesapla(program.efektifSira, sira) ?? dilimdenRisk(satir.dilim),
), ),
trendOzeti: satir.trendOzeti, trendOzeti: trendOzetiMetni(program.siraGecmisi),
program, 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.`;
}