feat: tadımlık havuzuna üniversite tipi boyutu

Tadımlık satırı batch üretildiği için sihirbazdaki devlet/vakıf seçimini
bilmiyor, vakıf seçen kullanıcıya devlet örneği gösteriyordu. Havuz
anahtarı kova×tür×kategori×tip'e genişledi (migration 0001; mevcut
satırlar tip='genel'). Üretim scripti tip varyantlarını uniturGrubu
filtresiyle seçer; tadimlikSec tip verildiğinde kategori→genel fallback'i
korur ama tip fallback'i YAPMAZ — uygun satır yoksa bileşen gizlenir,
yanlış tipte örnek asla gösterilmez. Client profildeki tipi de gönderir ve
SSR "genel" satırı tipe uymuyorsa fetch bitene kadar gizler.

Not: prod app.db volume'de — `pnpm tadimlik` prod DB'ye karşı ayrıca
koşulmalı.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-08-09 00:12:25 +03:00
parent 0c6c9da56c
commit 14f9b622b0
8 changed files with 1077 additions and 84 deletions

View File

@@ -1,11 +1,13 @@
/**
* Tadımlık havuzu batch üretimi: kova × puan türü × (kategori + "genel")
* kombinasyonları için 1'er örnek tercih satırı (gerekce/riskNotu/trendOzeti)
* üretip app.db'deki tadimlik_havuzu tablosuna yazar.
* Tadımlık havuzu batch üretimi: kova × puan türü × (kategori + "genel") ×
* (üniversite tipi: genel/devlet/vakıf) kombinasyonları için 1'er örnek
* tercih satırı (gerekce/riskNotu/trendOzeti) üretip app.db'deki
* tadimlik_havuzu tablosuna yazar.
*
* pnpm tadimlik # boş anahtarları doldurur (idempotent)
* pnpm tadimlik --force # mevcut satırları da yeniden üretir
* pnpm tadimlik --kova=10-50k --tur=say --kategori=muhendislik-teknoloji
* pnpm tadimlik --tip=vakif # yalnızca vakıf varyantları
* pnpm tadimlik --mock # anahtar olsa da mock metin (dev/test)
* pnpm tadimlik --bekleme=13000 # LLM çağrıları arası bekleme (ms)
*
@@ -24,10 +26,16 @@ import {
type DilimKey,
type Program,
type PuanTuruKey,
type UniturGrubu,
} from "../src/types/yokatlas";
import { KATEGORILER, kategoriEslesir } from "../src/lib/kategoriler";
import { havuzuKompaktJson, turLabel } from "../src/lib/rapor-havuzu";
import { GENEL_KATEGORI, KOVALAR, kategoriSlugBul } from "../src/lib/tadimlik-havuzu";
import {
GENEL_KATEGORI,
KOVALAR,
kategoriSlugBul,
type TadimlikTip,
} from "../src/lib/tadimlik-havuzu";
import { anahtarVar } from "../src/lib/ai/client";
import { yapilandirilmisUret } from "../src/lib/ai/cagri";
@@ -95,18 +103,22 @@ function programBilgisi(p: Program) {
/**
* Deterministik program seçimi: kovanın temsili sırası için "dengeli" dilimden
* (boşsa "garanti") kategoriye uyan, 2025'te en çok yerleşen alan program.
* Tanınır/dolu programlar tadımlıkta daha ikna edici.
* (boşsa "garanti") kategoriye ve üniversite tipine uyan, 2025'te en çok
* yerleşen alan program. Tanınır/dolu programlar tadımlıkta daha ikna edici.
*/
function programSec(
temsiliSira: number,
tur: PuanTuruKey,
kategori: string | null,
uniturGrubu?: UniturGrubu,
): { program: Program; dilim: DilimKey } | null {
for (const dilim of ["dengeli", "garanti"] as const) {
const { programlar } = dilimAra(temsiliSira, tur, dilim, { limit: 60 });
const { programlar } = dilimAra(temsiliSira, tur, dilim, {
limit: 60,
uniturGrubu,
});
const uygunlar = kategori
? programlar.filter((p) => kategoriEslesir(p.isim, [kategori]))
? programlar.filter((p) => kategoriEslesir(p.isim, [kategori], p.fakulte))
: programlar;
if (uygunlar.length === 0) continue;
const secilen = [...uygunlar].sort(
@@ -152,6 +164,7 @@ async function main() {
const kovaFiltre = bayrak("kova");
const turFiltre = bayrak("tur");
const kategoriFiltre = bayrak("kategori");
const tipFiltre = bayrak("tip");
const turlar = (Object.keys(PUAN_TURLERI) as PuanTuruKey[]).filter(
(t) => !turFiltre || t === turFiltre,
@@ -161,6 +174,9 @@ async function main() {
...KATEGORILER.map((ad) => ({ ad: ad as string, slug: kategoriSlugBul(ad) })),
].filter((k) => !kategoriFiltre || k.slug === kategoriFiltre);
const kovalar = KOVALAR.filter((k) => !kovaFiltre || k.slug === kovaFiltre);
const tipler = (["genel", "devlet", "vakif"] as TadimlikTip[]).filter(
(t) => !tipFiltre || t === tipFiltre,
);
const mevcutlar = new Set(
(
@@ -169,9 +185,10 @@ async function main() {
kovaSlug: schema.tadimlikHavuzu.kovaSlug,
tur: schema.tadimlikHavuzu.tur,
kategoriSlug: schema.tadimlikHavuzu.kategoriSlug,
tip: schema.tadimlikHavuzu.tip,
})
.from(schema.tadimlikHavuzu)
).map((s) => `${s.kovaSlug}|${s.tur}|${s.kategoriSlug}`),
).map((s) => `${s.kovaSlug}|${s.tur}|${s.kategoriSlug}|${s.tip}`),
);
if (mockAktif()) {
@@ -185,55 +202,46 @@ async function main() {
for (const kova of kovalar) {
for (const tur of turlar) {
for (const kategori of kategoriler) {
const anahtar = `${kova.slug}|${tur}|${kategori.slug}`;
if (!force && mevcutlar.has(anahtar)) {
atlananMevcut++;
continue;
}
for (const tip of tipler) {
const anahtar = `${kova.slug}|${tur}|${kategori.slug}|${tip}`;
if (!force && mevcutlar.has(anahtar)) {
atlananMevcut++;
continue;
}
const secim = programSec(kova.temsiliSira, tur, kategori.ad);
if (!secim) {
atlananBos++;
continue;
}
const metin = await metinUret(
secim.program,
secim.dilim,
kova.temsiliSira,
tur,
kategori.ad,
);
if (!metin) {
console.warn(`Şemaya uymayan çıktı, atlandı: ${anahtar}`);
atlananBos++;
continue;
}
const now = new Date();
await appDb
.insert(schema.tadimlikHavuzu)
.values({
id: randomUUID(),
kovaSlug: kova.slug,
const secim = programSec(
kova.temsiliSira,
tur,
kategoriSlug: kategori.slug,
programId: secim.program.id,
dilim: secim.dilim,
gerekce: metin.gerekce,
riskNotu: metin.riskNotu,
trendOzeti: metin.trendOzeti,
program: programBilgisi(secim.program),
ornekSira: kova.temsiliSira,
uretimAt: now,
})
.onConflictDoUpdate({
target: [
schema.tadimlikHavuzu.kovaSlug,
schema.tadimlikHavuzu.tur,
schema.tadimlikHavuzu.kategoriSlug,
],
set: {
kategori.ad,
tip === "genel" ? undefined : tip,
);
if (!secim) {
atlananBos++;
continue;
}
const metin = await metinUret(
secim.program,
secim.dilim,
kova.temsiliSira,
tur,
kategori.ad,
);
if (!metin) {
console.warn(`Şemaya uymayan çıktı, atlandı: ${anahtar}`);
atlananBos++;
continue;
}
const now = new Date();
await appDb
.insert(schema.tadimlikHavuzu)
.values({
id: randomUUID(),
kovaSlug: kova.slug,
tur,
kategoriSlug: kategori.slug,
tip,
programId: secim.program.id,
dilim: secim.dilim,
gerekce: metin.gerekce,
@@ -242,15 +250,35 @@ async function main() {
program: programBilgisi(secim.program),
ornekSira: kova.temsiliSira,
uretimAt: now,
},
});
})
.onConflictDoUpdate({
target: [
schema.tadimlikHavuzu.kovaSlug,
schema.tadimlikHavuzu.tur,
schema.tadimlikHavuzu.kategoriSlug,
schema.tadimlikHavuzu.tip,
],
set: {
programId: secim.program.id,
dilim: secim.dilim,
gerekce: metin.gerekce,
riskNotu: metin.riskNotu,
trendOzeti: metin.trendOzeti,
program: programBilgisi(secim.program),
ornekSira: kova.temsiliSira,
uretimAt: now,
},
});
uretilen++;
console.log(`${anahtar}${secim.program.isim} (${secim.dilim})`);
// Gerçek LLM'de rate limit dostu ardışık tempo
if (!mockAktif()) {
const bekleme = Number.parseInt(bayrak("bekleme") ?? "500", 10);
await new Promise((r) => setTimeout(r, bekleme));
uretilen++;
console.log(
`${anahtar}${secim.program.isim} (${secim.dilim})`,
);
// Gerçek LLM'de rate limit dostu ardışık tempo
if (!mockAktif()) {
const bekleme = Number.parseInt(bayrak("bekleme") ?? "500", 10);
await new Promise((r) => setTimeout(r, bekleme));
}
}
}
}