From 497b0247a0ab9b6d494ebc73696f7ef5adaf53e6 Mon Sep 17 00:00:00 2001
From: bilalgursen
Date: Mon, 5 Oct 2026 01:15:01 +0300
Subject: [PATCH] =?UTF-8?q?fix(havuz):=20B5=20=E2=80=94=20gev=C5=9Feyen=20?=
=?UTF-8?q?filtreye=20uyan=20programlar=20havuzda=20kals=C4=B1n=20[KARAR]?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
İl gevşeyince havuz baştan, ilsiz kuruluyordu; seçili ildeki programlar
sıraya en yakın 20'ye giremezse listeden tamamen düşüyordu (Kayseri 1/60,
Trabzon 0/25) — prompt ise "seçili illere öncelik ver" diyordu. Artık
gevşetmenin her basamağında seçime uyan programlar önce alınır, boşluk
sıraya en yakın diğer programlarla dolar. Aynı kural alan ve danışman
profili gevşediğinde de geçerli (tek yardımcı: oncelikliBirlestir). Kırpma
sonrası havuz yine dilim sırasında, dilim içinde yakından uzağa dizilir.
Kanıt (havuzda seçili ildeki program, önce → sonra): p12 Kayseri 1 → 9,
p13 Trabzon 0 → 4, b06 Düzce 2 → 10, a09 Konya 2 → 3. Gevşetme basamağı ve
liste uzunluğu hiçbir profilde değişmedi.
/meraklisina şeması, 4. bölüm anlatımı ve aria-label aynı kuralla
güncellendi.
Co-Authored-By: Claude Fable 5.1
---
.../pazarlama/components/huni-semasi.tsx | 4 +-
.../components/meraklisina-icerik.tsx | 5 +-
src/lib/rapor-havuzu.ts | 77 ++++++++++++++-----
3 files changed, 64 insertions(+), 22 deletions(-)
diff --git a/src/features/pazarlama/components/huni-semasi.tsx b/src/features/pazarlama/components/huni-semasi.tsx
index ee1f190..0bfc4f1 100644
--- a/src/features/pazarlama/components/huni-semasi.tsx
+++ b/src/features/pazarlama/components/huni-semasi.tsx
@@ -19,7 +19,7 @@ flowchart TD
C --> P("Danışmana söylediklerin
öğretim dili, uzaktan/örgün, burs, yıllık ücret sınırı, evden uzaklık — sohbette söyleyip kaydettiklerin varsa havuzu bunlar da süzer")
P --> H("Aday havuzu
elde kalan gerçek programlar — yalnızca en son tercih kılavuzunda yer alanlar
kapanan ya da kodu değişip eski kaydı kalan programlar önerilmez")
H --> S{"Havuzda 24'ten az program mı kaldı?"}
- S -- "evet" --> G("Filtreler adım adım gevşetilir
önce danışmana söylediklerin, sonra il, sonra alan — listede açıkça belirtilir")
+ S -- "evet" --> G("Filtreler adım adım gevşetilir
önce danışmana söylediklerin, sonra il, sonra alan — listede açıkça belirtilir
seçimine uyan programlar havuzda kalır, boşluk sıralamana en yakın diğer programlarla dolar")
G --> H
H --> U("Havuz yine de dar kalırsa
liste eldeki programlarla kurulur, dağılım havuza uyarlanır ve sana açıkça söylenir")
S -- "hayır" --> Y("Yapay zekâ havuzdan en fazla 24 tercih seçer
havuzda olmayan bölümü yazamaz, her satıra gerekçe yazar")
@@ -45,7 +45,7 @@ export function HuniSemasi() {
return (
Havuza yalnızca en son tercih kılavuzunda yer alan programlar girer;
diff --git a/src/features/pazarlama/components/meraklisina-icerik.tsx b/src/features/pazarlama/components/meraklisina-icerik.tsx
index 947ccac..dabdec5 100644
--- a/src/features/pazarlama/components/meraklisina-icerik.tsx
+++ b/src/features/pazarlama/components/meraklisina-icerik.tsx
@@ -128,7 +128,10 @@ export function MeraklisinaIcerik() {
gösterir — havuz 24'ün altına düşerse filtrelerin hangi
sırayla gevşetildiği (önce danışmana söylediklerin, sonra il,
sonra alan), yine de yetmezse listenin eldeki programlarla, sana
- açıkça belirtilerek nasıl kurulduğu da dahil.
+ açıkça belirtilerek nasıl kurulduğu da dahil. Bir filtre
+ gevşediğinde seçimine uyan programlar havuzdan çıkmaz: önce onlar
+ alınır, kalan boşluk sıralamana en yakın diğer programlarla
+ doldurulur.
diff --git a/src/lib/rapor-havuzu.ts b/src/lib/rapor-havuzu.ts
index aab3f16..afedc75 100644
--- a/src/lib/rapor-havuzu.ts
+++ b/src/lib/rapor-havuzu.ts
@@ -128,11 +128,14 @@ export function dilimHedefiHesapla(havuz: AdayProgram[]): DilimHedef {
// modellerde prefill + isabet maliyeti yüksek (ölçüm: 6 Ağu 2026).
const PROMPT_LIMIT_DILIM = 20;
+const DILIM_SIRASI: Record = { hayal: 0, dengeli: 1, garanti: 2 };
+
/**
- * Dilim sırasını koruyarak her dilimden en fazla n aday bırakır. Havuz tek
- * dilime yığılmışsa (ör. çok iyi derece: neredeyse hepsi güvenli) n'e kırpmak
- * listeyi 24'ün altına düşürürdü; o durumda eksik, adayı kalan dilimlerden
- * tamamlanır — yeterli program varken liste 24'ten kısa kalmaz.
+ * Her dilimden en fazla n aday bırakır; dizideki sıra önceliktir (öndeki
+ * kalır). Havuz tek dilime yığılmışsa (ör. çok iyi derece: neredeyse hepsi
+ * güvenli) n'e kırpmak listeyi 24'ün altına düşürürdü; o durumda eksik, adayı
+ * kalan dilimlerden tamamlanır — yeterli program varken liste 24'ten kısa
+ * kalmaz. Sonuç dilim sırasına ve dilim içinde yakından uzağa dizilir.
*/
function dilimBasinaKirp(havuz: AdayProgram[], n: number): AdayProgram[] {
const mevcut = dilimSay(havuz);
@@ -143,7 +146,34 @@ function dilimBasinaKirp(havuz: AdayProgram[], n: number): AdayProgram[] {
};
eksigiDagit(pay, mevcut, Math.min(HEDEF, havuz.length));
const sayac: DilimHedef = { hayal: 0, dengeli: 0, garanti: 0 };
- return havuz.filter((p) => ++sayac[p.dilim] <= pay[p.dilim]);
+ // hayal: sınıra en yakın (en büyük taban) önce; diğerleri en küçük taban önce
+ const yakinlik = (p: AdayProgram) =>
+ p.dilim === "hayal" ? -(p.efektifSira ?? 0) : (p.efektifSira ?? 0);
+ return havuz
+ .filter((p) => ++sayac[p.dilim] <= pay[p.dilim])
+ .sort(
+ (a, b) =>
+ DILIM_SIRASI[a.dilim] - DILIM_SIRASI[b.dilim] ||
+ yakinlik(a) - yakinlik(b),
+ );
+}
+
+/**
+ * Katmanları öncelik sırasıyla, tekrarsız birleştirir. Gevşetmede kullanılır:
+ * gevşeyen filtreye UYAN programlar önce gelir ve kırpmada korunur, kalan
+ * boşluk sonraki katmanlardan (sıraya en yakın diğer programlar) dolar.
+ */
+function oncelikliBirlestir(...katmanlar: AdayProgram[][]): AdayProgram[] {
+ const gorulen = new Set();
+ const sonuc: AdayProgram[] = [];
+ for (const katman of katmanlar) {
+ for (const p of katman) {
+ if (gorulen.has(p.id)) continue;
+ gorulen.add(p.id);
+ sonuc.push(p);
+ }
+ }
+ return sonuc;
}
function dilimEkle(r: {
@@ -167,6 +197,8 @@ function dilimEkle(r: {
*
* Süzgeçlerin hepsi dilimin TAMAMINA uygulanır, kırpma en sonda yapılır:
* "24'ten az" kararı o alanda/ilde gerçekten kaç program olduğuna bakar.
+ * Bir filtre gevşediğinde ona uyan programlar havuzdan düşmez: önce onlar
+ * alınır, boşluk sıraya en yakın diğer programlarla dolar.
*/
export function havuzOlustur(
params: RaporParams,
@@ -196,6 +228,8 @@ export function havuzOlustur(
// liste yine kurulur, gevşetme yapay zekâya ve adaya söylenir.
const suzgec = profilSuzgeci(profil, iller.length > 0);
const profilGevsetildi: Gevsetilen[] = [];
+ // Profil gevşerse profile uyan (24'ten az) programlar yine en önde tutulur
+ let profilliFiltreli: AdayProgram[] = [];
if (suzgec.ozellik || suzgec.iller) {
const profilliHam = ilFiltre(
dilimEkle(
@@ -206,7 +240,7 @@ export function havuzOlustur(
),
iller.length > 0 ? iller : suzgec.iller,
);
- const profilliFiltreli = kategoriFiltre(profilliHam);
+ profilliFiltreli = kategoriFiltre(profilliHam);
if (profilliFiltreli.length >= HEDEF) {
return {
havuz: dilimBasinaKirp(profilliFiltreli, PROMPT_LIMIT_DILIM),
@@ -222,35 +256,40 @@ export function havuzOlustur(
);
const tamHam = ilFiltre(ilsizHam, iller);
const tamFiltreli = kategoriFiltre(tamHam);
+ const kirp = (...katmanlar: AdayProgram[][]) =>
+ dilimBasinaKirp(
+ oncelikliBirlestir(profilliFiltreli, ...katmanlar),
+ PROMPT_LIMIT_DILIM,
+ );
if (tamFiltreli.length >= HEDEF) {
- return {
- havuz: dilimBasinaKirp(tamFiltreli, PROMPT_LIMIT_DILIM),
- gevsetildi: profilGevsetildi,
- };
+ return { havuz: kirp(tamFiltreli), gevsetildi: profilGevsetildi };
}
- // 2. Kategori gevşet: aynı illerdeki kategori-dışı programlarla doldur
+ // 2. Kategori gevşet: seçili alandakiler kalır, boşluk aynı illerdeki
+ // kategori-dışı programlarla dolar
if (kategoriler.length > 0 && tamHam.length >= HEDEF) {
return {
- havuz: dilimBasinaKirp(tamHam, PROMPT_LIMIT_DILIM),
+ havuz: kirp(tamFiltreli, tamHam),
gevsetildi: [...profilGevsetildi, "kategori"],
};
}
- // 3. İl gevşet: il filtresini kaldırıp kategoriye geri dön
+ // 3. İl gevşet: seçili ildekiler kalır, boşluk aynı kategorideki başka
+ // illerin programlarıyla dolar
const gevsetildi: Gevsetilen[] = [...profilGevsetildi];
if (iller.length > 0) gevsetildi.push("il");
const ilsizFiltreli = kategoriFiltre(ilsizHam);
if (ilsizFiltreli.length >= HEDEF) {
- return {
- havuz: dilimBasinaKirp(ilsizFiltreli, PROMPT_LIMIT_DILIM),
- gevsetildi,
- };
+ return { havuz: kirp(tamFiltreli, ilsizFiltreli), gevsetildi };
}
- // 4. Hem il hem kategori gevşet: elde ne varsa
+ // 4. Hem il hem kategori gevşet: önce iki seçime de uyanlar, sonra alana
+ // uyanlar, sonra ile uyanlar, en son elde ne varsa
if (kategoriler.length > 0) gevsetildi.push("kategori");
- return { havuz: dilimBasinaKirp(ilsizHam, PROMPT_LIMIT_DILIM), gevsetildi };
+ return {
+ havuz: kirp(tamFiltreli, ilsizFiltreli, tamHam, ilsizHam),
+ gevsetildi,
+ };
}
// Prompt'a veri penceresinin tamamı değil son 4 yılı gider (yeniden eskiye):