Yayın sonrası denetim (2026-09-21): - tadımlık havuzu snapshot'ı 2025 tabanını taşıyordu: örnek satır 'Tıp ~5.118, az riskli' derken aynı sayfadaki tablo 4.481 gösteriyordu. Taban ve sıra geçmişi artık okuma anında canlı DB'den; güncel kılavuzdan çıkmış program örnek gösterilmez (sıradaki kategoriye düşülür) - sunucuylaBirlestir riskleri her zaman yeniler: sunucu kaydı taze taban + başka cihazın bayat riskini taşıyabiliyordu Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
161 lines
6.4 KiB
TypeScript
161 lines
6.4 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";
|
||
import { ILK_YIL, VERI_YIL_SAYISI } from "./veri-yillari";
|
||
import { programGetir } from "./db";
|
||
|
||
/** 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. 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/;
|
||
|
||
// Bayat batch kalıbı: gerekçe "son N yıl" diyorsa N, verinin bugün kapsadığı
|
||
// yıl sayısı (VERI_YIL_SAYISI) olmalı. Veri penceresi uzayınca eski batch'in
|
||
// "son 5 yıl"ı da, daha eskisinin "son 4 yıl"ı da kendiliğinden takılır.
|
||
const SON_N_YIL = /son (\d+|[a-zçğıöşü]+) yıl/g;
|
||
const SAYI_SOZCUKLERI: Record<string, number> = {
|
||
bir: 1,
|
||
iki: 2,
|
||
üç: 3,
|
||
dört: 4,
|
||
beş: 5,
|
||
altı: 6,
|
||
yedi: 7,
|
||
sekiz: 8,
|
||
dokuz: 9,
|
||
on: 10,
|
||
};
|
||
|
||
function bayatYilSayisiVar(kucukMetin: string): boolean {
|
||
for (const [, ham] of kucukMetin.matchAll(SON_N_YIL)) {
|
||
const n = /^\d+$/.test(ham) ? Number(ham) : SAYI_SOZCUKLERI[ham];
|
||
// Sayı olmayan sözcük ("son birkaç yıl") yıl sayısı iddiası değildir
|
||
if (n != null && n !== VERI_YIL_SAYISI) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
export function gerekceServisEdilebilir(gerekce: string): boolean {
|
||
const kucuk = gerekce.toLocaleLowerCase("tr-TR");
|
||
return !YASAK_GEREKCE_DILI.test(kucuk) && !bayatYilSayisiVar(kucuk);
|
||
}
|
||
|
||
/**
|
||
* 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)) {
|
||
// Havuz batch'te üretilir ve yeni yerleştirme yılı yüklenince bayatlar:
|
||
// snapshot'taki taban/geçmiş yerine CANLI veri servis edilir ki örnek
|
||
// satır aynı sayfadaki tabloyla çelişmesin. Güncel kılavuzdan çıkmış
|
||
// (arşivlenmiş) program örnek gösterilmez — sıradaki kategoriye düşülür.
|
||
const canli = programGetir(satir.programId);
|
||
if (!canli) continue;
|
||
const program: RaporSonuc["programlar"][string] = {
|
||
...(satir.program as RaporSonuc["programlar"][string]),
|
||
efektifSira: canli.efektifSira,
|
||
siraGecmisi: canli.siraGecmisi,
|
||
ilkYil: ILK_YIL,
|
||
};
|
||
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.
|
||
// Snapshot'ın sıra dizisi üretildiği günün penceresidir (eski
|
||
// satırlarda 5 eleman); yıllar dizinin kendi çapasından okunur.
|
||
riskNotu: riskNotuMetni(
|
||
riskHesapla(program.efektifSira, sira) ?? dilimdenRisk(satir.dilim),
|
||
),
|
||
trendOzeti: trendOzetiMetni(program.siraGecmisi, program.ilkYil),
|
||
program,
|
||
};
|
||
}
|
||
}
|
||
return null;
|
||
}
|