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):