Files
kolaytercih/src/lib/tadimlik-havuzu.ts
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

121 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Girişsiz yapay zekâ tadımlığının sunucu katmanı: batch üretilmiş
// (scripts/tadimlik-uret.ts) satırlar app.db'deki tadimlik_havuzu'ndan
// SEÇİLİR — anonim istek LLM'e asla gitmez, seçim her zaman sunucuda.
import { and, eq } from "drizzle-orm";
import { appDb, schema } from "./appdb";
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";
/**
* Havuz anahtarındaki sıra kovaları. Eşikler analitik.ts'teki siraKovasi()
* ile birebir aynı tutulmalı (oradaki Türkçe etiketler DB anahtarı olamaz).
* temsiliSira: batch üretimde "bu kovadaki tipik aday" olarak kullanılan sıra.
*/
export const KOVALAR = [
{ slug: "0-10k", temsiliSira: 5_000, max: 10_000 },
{ slug: "10-50k", temsiliSira: 30_000, max: 50_000 },
{ slug: "50-100k", temsiliSira: 75_000, max: 100_000 },
{ slug: "100-250k", temsiliSira: 175_000, max: 250_000 },
{ slug: "250-500k", temsiliSira: 375_000, max: 500_000 },
{ slug: "500k-plus", temsiliSira: 750_000, max: Number.POSITIVE_INFINITY },
] as const;
export type Kova = (typeof KOVALAR)[number];
export function kovaBul(sira: number): Kova {
return KOVALAR.find((k) => sira <= k.max) ?? KOVALAR[KOVALAR.length - 1];
}
/** "Mühendislik & Teknoloji" → "muhendislik-teknoloji" (havuz anahtarı). */
export function kategoriSlugBul(kategori: string): string {
return turkishSlugify(kategori);
}
export interface TadimlikSatir {
programId: string;
dilim: Dilim;
gerekce: string;
riskNotu: string;
trendOzeti: string;
program: RaporSonuc["programlar"][string];
}
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
* bileşeni hiç render etmez (boş/generic tadımlık gösterilmez, PRD kuralı).
*
* 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,
tur: PuanTuruKey,
kategoriSlug?: string,
tip: TadimlikTip = "genel",
): Promise<TadimlikSatir | null> {
const kova = kovaBul(sira);
const sluglar = [
...new Set([kategoriSlug ?? GENEL_KATEGORI, GENEL_KATEGORI]),
];
for (const slug of sluglar) {
const satir = await appDb.query.tadimlikHavuzu.findFirst({
where: and(
eq(schema.tadimlikHavuzu.kovaSlug, kova.slug),
eq(schema.tadimlikHavuzu.tur, tur),
eq(schema.tadimlikHavuzu.kategoriSlug, slug),
eq(schema.tadimlikHavuzu.tip, tip),
),
});
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 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,
};
}
}
return null;
}