// 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 { 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; }