Files
kolaytercih/scripts/tadimlik-uret.ts
bilalgursen e62c6f07df
Some checks failed
Deploy / deploy (push) Failing after 1h28m6s
Update package.json scripts, enhance layout with new components, and improve API error handling
- Added new scripts for database operations and temporary production in package.json.
- Integrated KaydetBannerLazy component into the layout for improved user notifications.
- Enhanced API error handling in the soru route to mark credit exhaustion.
- Updated the IletisimPage for better button styling and user experience.
- Refactored ListemPage to streamline user flow and improve session handling.
- Removed unused ListePaneli component to clean up the codebase.
2026-08-06 02:27:42 +03:00

264 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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.
* Gemini ücretsiz katmanı dakikada ~5 istek kabul eder; gerçek üretimde
* --bekleme=13000 gibi bir tempo kullan.
* 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<z.infer<typeof TadimlikSchema> | 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");
// Not: Gemini'de thinking bütçesi de bu sınırdan düşer; dar tutulursa
// çıktı kırpılıp şema doğrulaması boşa düşüyor.
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);
});