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>
121 lines
4.8 KiB
TypeScript
121 lines
4.8 KiB
TypeScript
// 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;
|
||
}
|