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

@@ -0,0 +1,3 @@
DROP INDEX `tadimlik_anahtar`;--> statement-breakpoint
ALTER TABLE `tadimlik_havuzu` ADD `tip` text DEFAULT 'genel' NOT NULL;--> statement-breakpoint
CREATE UNIQUE INDEX `tadimlik_anahtar` ON `tadimlik_havuzu` (`kova_slug`,`tur`,`kategori_slug`,`tip`);

View File

@@ -0,0 +1,907 @@
{
"version": "6",
"dialect": "sqlite",
"id": "46e3e804-4222-4e4c-9151-dd71510cded5",
"prevId": "a802fded-01d5-4d17-8b6f-fca5dfc2920d",
"tables": {
"account": {
"name": "account",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"account_id": {
"name": "account_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"provider_id": {
"name": "provider_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"id_token": {
"name": "id_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"access_token_expires_at": {
"name": "access_token_expires_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"refresh_token_expires_at": {
"name": "refresh_token_expires_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"account_user_id_user_id_fk": {
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"chat_messages": {
"name": "chat_messages",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"client_message_id": {
"name": "client_message_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"chat_messages_client_message_id_unique": {
"name": "chat_messages_client_message_id_unique",
"columns": [
"client_message_id"
],
"isUnique": true
},
"chat_user_created": {
"name": "chat_user_created",
"columns": [
"user_id",
"created_at"
],
"isUnique": false
}
},
"foreignKeys": {
"chat_messages_user_id_user_id_fk": {
"name": "chat_messages_user_id_user_id_fk",
"tableFrom": "chat_messages",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"credit_ledger": {
"name": "credit_ledger",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"delta": {
"name": "delta",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"reason": {
"name": "reason",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"ref_id": {
"name": "ref_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"ledger_reason_ref": {
"name": "ledger_reason_ref",
"columns": [
"reason",
"ref_id"
],
"isUnique": true
},
"ledger_user": {
"name": "ledger_user",
"columns": [
"user_id"
],
"isUnique": false
}
},
"foreignKeys": {
"credit_ledger_user_id_user_id_fk": {
"name": "credit_ledger_user_id_user_id_fk",
"tableFrom": "credit_ledger",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"orders": {
"name": "orders",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"product": {
"name": "product",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"amount_kurus": {
"name": "amount_kurus",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"credits": {
"name": "credits",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'pending'"
},
"iyzico_token": {
"name": "iyzico_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"iyzico_payment_id": {
"name": "iyzico_payment_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"paid_at": {
"name": "paid_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"orders_user": {
"name": "orders_user",
"columns": [
"user_id"
],
"isUnique": false
}
},
"foreignKeys": {
"orders_user_id_user_id_fk": {
"name": "orders_user_id_user_id_fk",
"tableFrom": "orders",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"reports": {
"name": "reports",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"params": {
"name": "params",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"result": {
"name": "result",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"revision_count": {
"name": "revision_count",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"reports_user_id_unique": {
"name": "reports_user_id_unique",
"columns": [
"user_id"
],
"isUnique": true
}
},
"foreignKeys": {
"reports_user_id_user_id_fk": {
"name": "reports_user_id_user_id_fk",
"tableFrom": "reports",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"saved_lists": {
"name": "saved_lists",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"items": {
"name": "items",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"profil": {
"name": "profil",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"saved_lists_user_id_unique": {
"name": "saved_lists_user_id_unique",
"columns": [
"user_id"
],
"isUnique": true
}
},
"foreignKeys": {
"saved_lists_user_id_user_id_fk": {
"name": "saved_lists_user_id_user_id_fk",
"tableFrom": "saved_lists",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"session": {
"name": "session",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"ip_address": {
"name": "ip_address",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user_agent": {
"name": "user_agent",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"session_token_unique": {
"name": "session_token_unique",
"columns": [
"token"
],
"isUnique": true
}
},
"foreignKeys": {
"session_user_id_user_id_fk": {
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"tadimlik_havuzu": {
"name": "tadimlik_havuzu",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"kova_slug": {
"name": "kova_slug",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"tur": {
"name": "tur",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"kategori_slug": {
"name": "kategori_slug",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"tip": {
"name": "tip",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'genel'"
},
"program_id": {
"name": "program_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"dilim": {
"name": "dilim",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"gerekce": {
"name": "gerekce",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"risk_notu": {
"name": "risk_notu",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"trend_ozeti": {
"name": "trend_ozeti",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"program": {
"name": "program",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"ornek_sira": {
"name": "ornek_sira",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"uretim_at": {
"name": "uretim_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"tadimlik_anahtar": {
"name": "tadimlik_anahtar",
"columns": [
"kova_slug",
"tur",
"kategori_slug",
"tip"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user": {
"name": "user",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email_verified": {
"name": "email_verified",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"credit_balance": {
"name": "credit_balance",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"has_paket": {
"name": "has_paket",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"kredi_bitti_at": {
"name": "kredi_bitti_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"kredi_hatirlatma_gonderildi_at": {
"name": "kredi_hatirlatma_gonderildi_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"user_email_unique": {
"name": "user_email_unique",
"columns": [
"email"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"verification": {
"name": "verification",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@@ -8,6 +8,13 @@
"when": 1786053613118, "when": 1786053613118,
"tag": "0000_baslangic", "tag": "0000_baslangic",
"breakpoints": true "breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1786223252523,
"tag": "0001_steady_donald_blake",
"breakpoints": true
} }
] ]
} }

View File

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

View File

@@ -29,7 +29,18 @@ export async function GET(istek: Request) {
? kategoriParam ? kategoriParam
: undefined; : undefined;
const tadimlik = await tadimlikSec(sira, tur as PuanTuruKey, kategoriSlug); // Sihirbazdaki devlet/vakıf tercihi: uygun tipte satır yoksa null döner,
// yanlış tipte örnek asla servis edilmez.
const tipParam = url.searchParams.get("tip");
const tip =
tipParam === "devlet" || tipParam === "vakif" ? tipParam : "genel";
const tadimlik = await tadimlikSec(
sira,
tur as PuanTuruKey,
kategoriSlug,
tip,
);
return NextResponse.json( return NextResponse.json(
{ tadimlik }, { tadimlik },
// Satırlar kova granülaritesinde statik; 1 saat cache yeterli. // Satırlar kova granülaritesinde statik; 1 saat cache yeterli.

View File

@@ -27,10 +27,17 @@ import { dilimdenRisk, riskHesapla } from "@/lib/risk";
import { turkishSlugify } from "@/lib/slug"; import { turkishSlugify } from "@/lib/slug";
import { PROFIL_DEGISTI_EVENT, sonucHref } from "@/features/sihirbaz/sihirbaz-sabitler"; import { PROFIL_DEGISTI_EVENT, sonucHref } from "@/features/sihirbaz/sihirbaz-sabitler";
import { tercihProfiliOku } from "@/features/sihirbaz/sihirbaz-profil"; import { tercihProfiliOku } from "@/features/sihirbaz/sihirbaz-profil";
import type { TadimlikSatir } from "@/lib/tadimlik-havuzu"; import type { TadimlikSatir, TadimlikTip } from "@/lib/tadimlik-havuzu";
const GENEL = "genel"; const GENEL = "genel";
/** Program tipi ile sihirbaz tercihi eşleşiyor mu (uniturFiltreSql semantiği). */
function tipeUyar(program: TadimlikSatir["program"], tip: TadimlikTip): boolean {
if (tip === "genel") return true;
if (tip === "devlet") return program.unitur === "DEVLET";
return program.unitur != null && program.unitur !== "DEVLET";
}
export function TadimlikSatiri({ export function TadimlikSatiri({
sira, sira,
tur, tur,
@@ -42,29 +49,46 @@ export function TadimlikSatiri({
ilk: TadimlikSatir | null; ilk: TadimlikSatir | null;
}) { }) {
const [satir, setSatir] = useState<TadimlikSatir | null>(ilk); const [satir, setSatir] = useState<TadimlikSatir | null>(ilk);
const yuklenenSlug = useRef(GENEL); const yuklenenAnahtar = useRef(`${GENEL}|${GENEL}`);
// Sihirbaz kategorisi yalnızca localStorage'da yaşar; mount'ta ve profil // Sihirbaz kategorisi ve devlet/vakıf tercihi yalnızca localStorage'da
// değiştikçe okunur, kategoriye özel satır sunucudan çekilir. // yaşar; mount'ta ve profil değiştikçe okunur, seçime özel satır sunucudan
// çekilir. Tip tercihi varsa yanlış tipte örnek bir an bile gösterilmez.
useEffect(() => { useEffect(() => {
const guncelle = () => { const guncelle = () => {
const profil = tercihProfiliOku(); const profil = tercihProfiliOku();
const kategori = profil?.secimler.kategoriler[0]; const kategori = profil?.secimler.kategoriler[0];
const slug = kategori ? turkishSlugify(kategori) : GENEL; const slug = kategori ? turkishSlugify(kategori) : GENEL;
if (slug === yuklenenSlug.current) return; const tipTercihi = profil?.secimler.universiteTipi;
yuklenenSlug.current = slug; const tip: TadimlikTip =
if (slug === GENEL) { tipTercihi === "devlet" || tipTercihi === "vakif"
? tipTercihi
: GENEL;
const anahtar = `${slug}|${tip}`;
if (anahtar === yuklenenAnahtar.current) return;
yuklenenAnahtar.current = anahtar;
if (slug === GENEL && tip === GENEL) {
setSatir(ilk); setSatir(ilk);
return; return;
} }
fetch( // SSR "genel" satırı seçilen tipe uymuyorsa fetch bitene kadar gizle
`/api/tadimlik?sira=${sira}&tur=${encodeURIComponent(tur)}&kategori=${encodeURIComponent(slug)}`, setSatir((mevcut) =>
) mevcut && !tipeUyar(mevcut.program, tip) ? null : mevcut,
);
const q = new URLSearchParams({ sira: String(sira), tur });
if (slug !== GENEL) q.set("kategori", slug);
if (tip !== GENEL) q.set("tip", tip);
fetch(`/api/tadimlik?${q}`)
.then((yanit) => (yanit.ok ? yanit.json() : null)) .then((yanit) => (yanit.ok ? yanit.json() : null))
.then((veri: { tadimlik: TadimlikSatir | null } | null) => { .then((veri: { tadimlik: TadimlikSatir | null } | null) => {
// Yarış koruması: bu arada başka kategori yüklendiyse dokunma // Yarış koruması: bu arada başka seçim yüklendiyse dokunma
if (yuklenenSlug.current !== slug) return; if (yuklenenAnahtar.current !== anahtar) return;
if (veri?.tadimlik) setSatir(veri.tadimlik); if (veri?.tadimlik) {
setSatir(veri.tadimlik);
} else if (tip !== GENEL) {
// Uygun tipte satır yok: yanlış tip göstermektense gizle
setSatir(null);
}
}) })
.catch(() => {}); .catch(() => {});
}; };

View File

@@ -154,8 +154,9 @@ export const reports = sqliteTable("reports", {
}); });
// Anonim /sonuc tadımlığı: batch üretilmiş tek tercih satırları. // Anonim /sonuc tadımlığı: batch üretilmiş tek tercih satırları.
// Anahtar: sıra kovası × puan türü × kategori ("genel" = kategorisiz fallback). // Anahtar: sıra kovası × puan türü × kategori × üniversite tipi
// Anonim istek LLM'e asla gitmez; bu tablo salt-okunur servis edilir. // ("genel" = kategorisiz/tipsiz fallback). Anonim istek LLM'e asla gitmez;
// bu tablo salt-okunur servis edilir.
// NOT: Yeni tablo/kolon eklerken `pnpm db:generate` ile migration üretilmeli; // NOT: Yeni tablo/kolon eklerken `pnpm db:generate` ile migration üretilmeli;
// prod şemasıılışta yalnızca drizzle/ altındaki migration'larla güncellenir. // prod şemasıılışta yalnızca drizzle/ altındaki migration'larla güncellenir.
export const tadimlikHavuzu = sqliteTable( export const tadimlikHavuzu = sqliteTable(
@@ -165,6 +166,11 @@ export const tadimlikHavuzu = sqliteTable(
kovaSlug: text("kova_slug").notNull(), kovaSlug: text("kova_slug").notNull(),
tur: text("tur", { enum: ["say", "ea", "soz", "dil", "tyt"] }).notNull(), tur: text("tur", { enum: ["say", "ea", "soz", "dil", "tyt"] }).notNull(),
kategoriSlug: text("kategori_slug").notNull(), kategoriSlug: text("kategori_slug").notNull(),
// Sihirbazdaki devlet/vakıf tercihine uyan varyant; "genel" = tip
// filtresi olmayan satır (tipsiz kullanıcı bunu görür).
tip: text("tip", { enum: ["genel", "devlet", "vakif"] })
.notNull()
.default("genel"),
programId: text("program_id").notNull(), programId: text("program_id").notNull(),
dilim: text("dilim", { enum: ["hayal", "dengeli", "garanti"] }).notNull(), dilim: text("dilim", { enum: ["hayal", "dengeli", "garanti"] }).notNull(),
gerekce: text("gerekce").notNull(), gerekce: text("gerekce").notNull(),
@@ -178,7 +184,7 @@ export const tadimlikHavuzu = sqliteTable(
uretimAt: integer("uretim_at", { mode: "timestamp" }).notNull(), uretimAt: integer("uretim_at", { mode: "timestamp" }).notNull(),
}, },
(t) => [ (t) => [
uniqueIndex("tadimlik_anahtar").on(t.kovaSlug, t.tur, t.kategoriSlug), uniqueIndex("tadimlik_anahtar").on(t.kovaSlug, t.tur, t.kategoriSlug, t.tip),
], ],
); );

View File

@@ -46,15 +46,21 @@ export interface TadimlikSatir {
program: RaporSonuc["programlar"][string]; program: RaporSonuc["programlar"][string];
} }
export type TadimlikTip = "genel" | "devlet" | "vakif";
/** /**
* Kova × tür × kategori anahtarıyla tadımlık satırı seçer. Kategoriye özel * Kova × tür × kategori × tip anahtarıyla tadımlık satırı seçer. Kategoriye
* satır yoksa "genel"e düşer; o da yoksa null — çağıran taraf bileşeni hiç * özel satır yoksa "genel" kategoriye düşer; o da yoksa null — çağıran taraf
* render etmez (boş/generic tadımlık gösterilmez, PRD kuralı). * 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.
*/ */
export async function tadimlikSec( export async function tadimlikSec(
sira: number, sira: number,
tur: PuanTuruKey, tur: PuanTuruKey,
kategoriSlug?: string, kategoriSlug?: string,
tip: TadimlikTip = "genel",
): Promise<TadimlikSatir | null> { ): Promise<TadimlikSatir | null> {
const kova = kovaBul(sira); const kova = kovaBul(sira);
const sluglar = [ const sluglar = [
@@ -66,6 +72,7 @@ export async function tadimlikSec(
eq(schema.tadimlikHavuzu.kovaSlug, kova.slug), eq(schema.tadimlikHavuzu.kovaSlug, kova.slug),
eq(schema.tadimlikHavuzu.tur, tur), eq(schema.tadimlikHavuzu.tur, tur),
eq(schema.tadimlikHavuzu.kategoriSlug, slug), eq(schema.tadimlikHavuzu.kategoriSlug, slug),
eq(schema.tadimlikHavuzu.tip, tip),
), ),
}); });
if (satir) { if (satir) {