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

@@ -154,8 +154,9 @@ export const reports = sqliteTable("reports", {
});
// Anonim /sonuc tadımlığı: batch üretilmiş tek tercih satırları.
// Anahtar: sıra kovası × puan türü × kategori ("genel" = kategorisiz fallback).
// Anonim istek LLM'e asla gitmez; bu tablo salt-okunur servis edilir.
// Anahtar: sıra kovası × puan türü × kategori × üniversite tipi
// ("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;
// prod şeması açılışta yalnızca drizzle/ altındaki migration'larla güncellenir.
export const tadimlikHavuzu = sqliteTable(
@@ -165,6 +166,11 @@ export const tadimlikHavuzu = sqliteTable(
kovaSlug: text("kova_slug").notNull(),
tur: text("tur", { enum: ["say", "ea", "soz", "dil", "tyt"] }).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(),
dilim: text("dilim", { enum: ["hayal", "dengeli", "garanti"] }).notNull(),
gerekce: text("gerekce").notNull(),
@@ -178,7 +184,7 @@ export const tadimlikHavuzu = sqliteTable(
uretimAt: integer("uretim_at", { mode: "timestamp" }).notNull(),
},
(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];
}
export type TadimlikTip = "genel" | "devlet" | "vakif";
/**
* Kova × tür × kategori anahtarıyla tadımlık satırı seçer. Kategoriye özel
* satır yoksa "genel"e düşer; o da yoksa null — çağıran taraf bileşeni hiç
* render etmez (boş/generic tadımlık gösterilmez, PRD kuralı).
* 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.
*/
export async function tadimlikSec(
sira: number,
tur: PuanTuruKey,
kategoriSlug?: string,
tip: TadimlikTip = "genel",
): Promise<TadimlikSatir | null> {
const kova = kovaBul(sira);
const sluglar = [
@@ -66,6 +72,7 @@ export async function tadimlikSec(
eq(schema.tadimlikHavuzu.kovaSlug, kova.slug),
eq(schema.tadimlikHavuzu.tur, tur),
eq(schema.tadimlikHavuzu.kategoriSlug, slug),
eq(schema.tadimlikHavuzu.tip, tip),
),
});
if (satir) {