/** * 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. * * 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 --mock # anahtar olsa da mock metin (dev/test) * pnpm tadimlik --bekleme=13000 # LLM çağrıları arası bekleme (ms) * * AI anahtarı yoksa (dev) mock metin üretir — akış uçtan uca test edilebilir. * OpenRouter'da katı bir dakikalık limit yok; --bekleme varsayılanı (500 ms) * genelde yeterli, 429 görürsen tempoyu artır. * Anonim trafik LLM'e asla istek atmaz; tek maliyet bu script'in koşusudur. */ import { randomUUID } from "node:crypto"; import { existsSync, readFileSync } from "node:fs"; import { z } from "zod"; import { appDb, schema } from "../src/lib/appdb"; import { dilimAra, type DilimKey, type Program, type PuanTuruKey, PUAN_TURLERI } from "../src/lib/db"; 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 { anahtarVar } from "../src/lib/ai/client"; import { yapilandirilmisUret } from "../src/lib/ai/cagri"; // Next dışında koşuyoruz; .env dosyalarını elle yükle (mevcut env kazanır). function envYukle(dosya: string): void { if (!existsSync(dosya)) return; for (const satir of readFileSync(dosya, "utf8").split("\n")) { const m = satir.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/); if (m && process.env[m[1]] === undefined) { process.env[m[1]] = m[2].replace(/^"(.*)"$/, "$1").trim(); } } } envYukle(".env.local"); envYukle(".env"); const TadimlikSchema = z.object({ gerekce: z.string(), riskNotu: z.string(), trendOzeti: z.string(), }); // rapor.ts'teki SISTEM kurallarının tek satırlık versiyonu. const SISTEM = `Sen KolayTercih'in kıdemli YKS tercih danışmanısın. Sana GERÇEK YÖK Atlas verisiyle TEK bir program ve temsili bir aday tanımı verilecek; bu program için tercih listesi satırı metinleri üreteceksin. KURALLAR (kesin): - METİN DİYETİ: her alan TEK kısa cümle, dolgu söz yok. - gerekce: bu programın bu aday için neden mantıklı olduğu (TEK cümle, en fazla ~15 kelime, Türkçe). - riskNotu: yerleşme ihtimali değerlendirmesi (TEK kısa cümle). ASLA "garanti", "kesin", "yüzde yüz" deme; "yüksek/orta/düşük ihtimal" dili kullan. - trendOzeti: son 4 yılın taban sıralaması hareketi, TEK cümle (ör. "2022'den beri taban 45B'den 38B'ye indi, bölüm zorlaşıyor"). - Adayın sıralaması yaklaşıktır; kesin sıra telaffuz etme, banda göre konuş.`; function mockAktif(): boolean { if (process.argv.includes("--mock")) return true; return process.env.NODE_ENV !== "production" && !anahtarVar(); } function mockMetin(p: Program, temsiliSira: number, kategori: string | null) { return { gerekce: `(Dev mock) ${p.isim}, ${kategori ? `${kategori} ilgisiyle ve ` : ""}~${temsiliSira.toLocaleString("tr-TR")} bandındaki sıralamanla uyumlu bir aday.`, riskNotu: "(Dev mock) Taban sıralaması senin civarında; orta-yüksek ihtimalle yerleşilebilir.", trendOzeti: "(Dev mock) Son 4 yılda taban sıralaması görece stabil seyretti.", }; } /** rapor.ts programBilgisi ile aynı snapshot şekli. */ function programBilgisi(p: Program) { const gecmis = [p.sira2021, p.sira2022, p.sira2023, p.sira2024, p.sira2025]; return { isim: p.isim, universite: p.universite, il: p.il, unitur: p.unitur, efektifSira: p.sira2025 ?? p.sira2024, siraGecmisi: gecmis.some((s) => s != null) ? gecmis : undefined, }; } /** * 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. */ function programSec( temsiliSira: number, tur: PuanTuruKey, kategori: string | null, ): { program: Program; dilim: DilimKey } | null { for (const dilim of ["dengeli", "garanti"] as const) { const { programlar } = dilimAra(temsiliSira, tur, dilim, { limit: 60 }); const uygunlar = kategori ? programlar.filter((p) => kategoriEslesir(p.isim, [kategori])) : programlar; if (uygunlar.length === 0) continue; const secilen = [...uygunlar].sort( (a, b) => (b.yerlesen2025 ?? -1) - (a.yerlesen2025 ?? -1), )[0]; return { program: secilen, dilim }; } return null; } async function metinUret( p: Program, dilim: DilimKey, temsiliSira: number, tur: PuanTuruKey, kategori: string | null, ): Promise | null> { if (mockAktif()) return mockMetin(p, temsiliSira, kategori); const kullanici = [ `Temsili aday: başarı sıralaması ~${temsiliSira.toLocaleString("tr-TR")} bandında, puan türü ${turLabel(tur)}.`, kategori ? `Adayın ilgi alanı: ${kategori}.` : null, `PROGRAM (yalnızca bunun için metin üret): ${havuzuKompaktJson([{ ...p, dilim }])}`, ] .filter(Boolean) .join("\n\n"); return yapilandirilmisUret({ system: SISTEM, kullanici, sema: TadimlikSchema, maxTokens: 4000, }); } function bayrak(ad: string): string | undefined { const arg = process.argv.find((a) => a.startsWith(`--${ad}=`)); return arg?.slice(ad.length + 3); } async function main() { const force = process.argv.includes("--force"); const kovaFiltre = bayrak("kova"); const turFiltre = bayrak("tur"); const kategoriFiltre = bayrak("kategori"); const turlar = (Object.keys(PUAN_TURLERI) as PuanTuruKey[]).filter( (t) => !turFiltre || t === turFiltre, ); const kategoriler: { ad: string | null; slug: string }[] = [ { ad: null, slug: GENEL_KATEGORI }, ...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 mevcutlar = new Set( ( await appDb .select({ kovaSlug: schema.tadimlikHavuzu.kovaSlug, tur: schema.tadimlikHavuzu.tur, kategoriSlug: schema.tadimlikHavuzu.kategoriSlug, }) .from(schema.tadimlikHavuzu) ).map((s) => `${s.kovaSlug}|${s.tur}|${s.kategoriSlug}`), ); if (mockAktif()) { console.log("AI anahtarı yok — mock metinlerle üretiliyor (dev)."); } let uretilen = 0; let atlananMevcut = 0; let atlananBos = 0; 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; } 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, 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: { 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)); } } } } console.log( `Bitti: ${uretilen} üretildi, ${atlananMevcut} mevcut atlandı, ${atlananBos} içeriksiz/şemasız atlandı.`, ); } main().catch((hata) => { console.error(hata); process.exit(1); });