feat(liste): danışman profili havuzu süzer (önce profil gevşetilir) + KVKK metin düzeltmeleri

- db.ts OzellikSuzgeci (dil, yalnız örgün, yalnız burslu, ücret üst sınırı)
  program_ozellik alt sorgusuyla; tablo yoksa süzgeç yok.
- rapor-havuzu: profilSuzgeci (ev ili + km → il listesi; sihirbazda il
  seçiliyse km uygulanmaz); havuz 24'ün altına düşerse gevşetme sırası
  profil → il → kategori. rapor.ts prompt'a 'danışmana söylediği tercihler'
  ve profil gevşetme notu; rapor-actions profili okuyup geçirir.
- /meraklisina şeması (huni-semasi SEMA + aria-label + figcaption) ve 4.
  bölüm metni yeni adım ve gevşetme sırasıyla eşitlendi (AGENTS.md kuralı).
- /api/soru: profil kaydı öncesi 'kaydedeyim mi?' onayı (PROFIL_ONAY_SOR).
- Gizlilik metni kodla eşitlendi: OpenRouter→DeepSeek (Anthropic değil),
  profil satırı, amaç+hukuki sebep, saklama süreleri, md. 11 hakları,
  18 yaş altı notu; unvan/adres ve aktarım dayanağı TODO. Giriş kartı:
  koşullar 'kabul', gizlilik 'okudum'. Sohbet girişine kişisel bilgi uyarısı.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-25 01:23:14 +03:00
parent aa2051732d
commit 5a04db7e4b
11 changed files with 385 additions and 86 deletions

View File

@@ -1,6 +1,8 @@
// server-only guard'ı bilinçli yok: scripts/tadimlik-uret.ts bu modülü düz
// node (tsx) altında import ediyor; server-only paketi orada fırlar.
import { searchByRank } from "./db";
import { searchByRank, type OzellikSuzgeci } from "./db";
import type { DanismanProfili } from "./danisman-profil";
import { yakinIller } from "./mesafe";
import {
PUAN_TURLERI,
type Program,
@@ -16,10 +18,55 @@ export interface AdayProgram extends Program {
dilim: Dilim;
}
export type Gevsetilen = "profil" | "kategori" | "il";
export interface HavuzSonuc {
havuz: AdayProgram[];
// Havuz 24'e ulaşmak için hangi filtreler gevşetildi
gevsetildi: ("kategori" | "il")[];
// Havuz 24'e ulaşmak için hangi filtreler gevşetildi (sırayla)
gevsetildi: Gevsetilen[];
}
/**
* Danışman profilinden sert süzgeç: dil, öğretim türü, burs, ücret üst sınırı
* (program_ozellik) ve ev iline km sınırı (il listesi). Sihirbazda il
* seçilmişse km sınırı uygulanmaz — sihirbaz seçimi daha yeni ve açık.
*/
export function profilSuzgeci(
profil: DanismanProfili | undefined,
sihirbazIlVar: boolean,
): { ozellik?: OzellikSuzgeci; iller?: string[]; ozet: string[] } {
if (!profil) return { ozet: [] };
const ozellik: OzellikSuzgeci = {};
const ozet: string[] = [];
if (profil.dil === "ingilizce" || profil.dil === "turkce") {
ozellik.dil = profil.dil === "ingilizce" ? "İngilizce" : "Türkçe";
ozet.push(`öğretim dili ${ozellik.dil}`);
}
if (profil.ogretimTuru === "yalniz-orgun") {
ozellik.yalnizOrgun = true;
ozet.push("yalnız örgün öğretim");
}
if (profil.burs === "yalniz-burslu") {
ozellik.yalnizBurslu = true;
ozet.push("vakıfta yalnız burslu");
}
if (profil.ucretUst) {
ozellik.ucretUst = profil.ucretUst;
ozet.push(`yıllık ücret en fazla ${profil.ucretUst.toLocaleString("tr-TR")} TL`);
}
let iller: string[] | undefined;
if (!sihirbazIlVar && profil.evIl && profil.maxKm) {
const yakin = yakinIller(profil.evIl, profil.maxKm);
if (yakin.length > 0) {
iller = yakin;
ozet.push(`${profil.evIl} iline en fazla ${profil.maxKm} km`);
}
}
return {
ozellik: Object.keys(ozellik).length ? ozellik : undefined,
iller,
ozet,
};
}
const HEDEF = 24;
@@ -89,12 +136,16 @@ function dilimEkle(r: {
}
/**
* Deterministik aday havuzu: Claude bu havuzdan SEÇER, asla uydurmaz.
* Sihirbaz seçimlerine (kategori/il/üniversite tipi) göre filtreler; havuz
* 24'ün altına düşerse kademeli gevşetir (önce il, sonra kategori) ki liste
* her zaman kurulabilsin.
* Deterministik aday havuzu: yapay zekâ bu havuzdan SEÇER, asla uydurmaz.
* Sihirbaz seçimlerine (kategori/il/üniversite tipi) ve varsa danışman
* profiline (dil, öğretim türü, burs, ücret, km) göre filtreler; havuz 24'ün
* altına düşerse kademeli gevşetir (önce profil, sonra il, sonra kategori) ki
* liste her zaman kurulabilsin. /meraklisina şeması bu sırayı anlatır.
*/
export function havuzOlustur(params: RaporParams): HavuzSonuc {
export function havuzOlustur(
params: RaporParams,
profil?: DanismanProfili,
): HavuzSonuc {
const uniturGrubu =
params.universiteTipi && params.universiteTipi !== "farketmez"
? params.universiteTipi
@@ -110,6 +161,30 @@ export function havuzOlustur(params: RaporParams): HavuzSonuc {
? havuz.filter((p) => kategoriEslesir(p.isim, kategoriler, p.fakulte))
: havuz;
// 0. Profil süzgeci (danışmana söylenenler) — yalnız yeterli havuz bırakırsa.
// Bırakmazsa profil gevşetilir ve aşağıdaki sihirbaz akışı aynen sürer;
// liste yine kurulur, gevşetme yapay zekâya ve adaya söylenir.
const suzgec = profilSuzgeci(profil, iller.length > 0);
const profilGevsetildi: Gevsetilen[] = [];
if (suzgec.ozellik || suzgec.iller) {
const profilliHam = dilimEkle(
searchByRank(params.sira, params.tur, {
iller: iller.length > 0 ? iller : suzgec.iller,
uniturGrubu,
ozellik: suzgec.ozellik,
limitPerBucket: LIMIT,
}),
);
const profilliFiltreli = kategoriFiltre(profilliHam);
if (profilliFiltreli.length >= HEDEF) {
return {
havuz: dilimBasinaKirp(profilliFiltreli, PROMPT_LIMIT_DILIM),
gevsetildi: [],
};
}
profilGevsetildi.push("profil");
}
// 1. Tam filtre: iller + üniversite tipi (SQL), kategori (JS)
const tamHam = dilimEkle(
searchByRank(params.sira, params.tur, {
@@ -122,7 +197,7 @@ export function havuzOlustur(params: RaporParams): HavuzSonuc {
if (tamFiltreli.length >= HEDEF) {
return {
havuz: dilimBasinaKirp(tamFiltreli, PROMPT_LIMIT_DILIM),
gevsetildi: [],
gevsetildi: profilGevsetildi,
};
}
@@ -130,12 +205,12 @@ export function havuzOlustur(params: RaporParams): HavuzSonuc {
if (kategoriler.length > 0 && tamHam.length >= HEDEF) {
return {
havuz: dilimBasinaKirp(tamHam, PROMPT_LIMIT_DILIM),
gevsetildi: ["kategori"],
gevsetildi: [...profilGevsetildi, "kategori"],
};
}
// 3. İl gevşet: il filtresini kaldırıp kategoriye geri dön
const gevsetildi: ("kategori" | "il")[] = [];
const gevsetildi: Gevsetilen[] = [...profilGevsetildi];
if (iller.length > 0) gevsetildi.push("il");
const ilsizHam = dilimEkle(
searchByRank(params.sira, params.tur, {