fix(havuz): "ilk N" ölçütü yalnız ilk N satırı belirler; öncelikliye kırpmada N+pay kota

"İlk 5 İstanbul" isteğinde öncelikliler dilim içinde sınırsız öne alınınca
havuz 60/60 İstanbul oluyor, 24 satırın hepsi İstanbul'a kayıyordu. Kırpma
artık öncelikliye yalnız adet+2 yer ayırır (dilimlere sırayla), kalan kota
sihirbaz süzgecine uyan adaylara gider. revizyon-denetim ilk5-istanbul
senaryosu kalan 19'un çoğunluğunu sihirbaz illerinden bekliyor; 5/5 geçti.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-08 23:27:13 +03:00
parent e4c06f54d9
commit 91e285900e
2 changed files with 81 additions and 14 deletions

View File

@@ -177,6 +177,54 @@ function dilimBasinaKirp(havuz: AdayProgram[], n: number): AdayProgram[] {
);
}
const DILIMLER: Dilim[] = ["hayal", "dengeli", "garanti"];
/**
* Kırpmada "ilk N" ölçütüne uyan programlara ayrılan yer: N + pay. Pay,
* modelin ya da garanti takasının (revizyon-kurali.ts) N'yi bulabilmesi için
* küçük bir esneklik; fazlası kalan satırları da ölçüte kaydırırdı.
*/
const ONCELIKLI_PAY = 2;
/**
* "İlk N" ölçütü yalnız ilk N satırı belirler; kalan satırlar sihirbaz
* seçimine göre kalır (ürün kararı, 7 Eki 2026). Önceden öncelikliler dilim
* içinde sınırsız öne alınıyordu: her dilimde 20+ programı olan il (İstanbul)
* havuzu 60/60 tek başına dolduruyor, "ilk 5 İstanbul" isteği 24 satırın
* hepsini İstanbul yapıyordu. Şimdi öncelikliye toplam `kota` yer ayrılır —
* dilimlere sırayla birer birer, adayı kalan dilime (hayal → dengeli →
* güvenli) — kalan kota sihirbaz süzgecine uyan sıradan adaylara gider;
* sıradan yetmezse boşluk yine öncelikliden dolar (dilimBasinaKirp öndekini
* tutar). Dizi sırası korunur: katman, sonra uyum/yakınlık.
*/
function oncelikliyeKotaAyir(
havuz: AdayProgram[],
oncelikli: (p: AdayProgram) => boolean,
kota: number,
): AdayProgram[] {
const one: Record<Dilim, AdayProgram[]> = { hayal: [], dengeli: [], garanti: [] };
const siradan: Record<Dilim, AdayProgram[]> = { hayal: [], dengeli: [], garanti: [] };
for (const p of havuz) (oncelikli(p) ? one : siradan)[p.dilim].push(p);
const pay: DilimHedef = { hayal: 0, dengeli: 0, garanti: 0 };
let kalan = kota;
while (kalan > 0) {
let verildi = false;
for (const d of DILIMLER) {
if (kalan > 0 && one[d].length > pay[d]) {
pay[d]++;
kalan--;
verildi = true;
}
}
if (!verildi) break;
}
return DILIMLER.flatMap((d) => [
...one[d].slice(0, pay[d]),
...siradan[d],
...one[d].slice(pay[d]),
]);
}
/** Revizyonun "ekle" kimlikleri: dilim evreninde bulunanlar, katman 0 ile. */
function ekstraSec(evren: AdayProgram[], idler?: readonly string[]): AdayProgram[] {
if (!idler?.length) return [];
@@ -282,7 +330,9 @@ function dilimEkle(r: {
* Sohbet revizyonunun havuza dokunan kısmı (lib/ai/rapor.ts kurar):
* `cikar` eşleşenler hiçbir katmana girmez; `oncelikli` eşleşenler kırpmadan
* önce dilim içinde öne alınır ("ilk 5 İstanbul olsun" için İstanbul havuzda
* kalsın); `ekstraIdler` adayın açıkça istediği programlar — sihirbaz
* kalsın) — ama yalnız `params.revizyon.ilkTercihler.adet` + pay kadar yer
* alır (oncelikliyeKotaAyir), kalan havuz sihirbaz seçiminden kurulur;
* `ekstraIdler` adayın açıkça istediği programlar — sihirbaz
* süzgecine takılsa bile (baraj ve özel kontenjan kuralı yine geçer) havuza
* kendi dilimiyle eklenir.
*/
@@ -339,14 +389,22 @@ export function havuzOlustur(
const ekstra = ekstraSec(ilsizHam, secenekler.ekstraIdler);
// Revizyon önceliği gevşetme katmanından sonra, uyum puanından önce gelir:
// kararlı sıralama, eşleşenler dilim içinde öne (kırpma öndekileri tutar).
const oncelikle = (havuz: AdayProgram[]) =>
secenekler.oncelikli
? [...havuz].sort(
(a, b) =>
(a.katman ?? 0) - (b.katman ?? 0) ||
Number(secenekler.oncelikli!(b)) - Number(secenekler.oncelikli!(a)),
)
: havuz;
// "İlk N" adedi biliniyorsa öncelikliye yalnız N + pay yer ayrılır; kalan
// kırpma kotası sihirbaz seçimine uyan sıradan adaylara kalır.
const ilkNAdet = params.revizyon?.ilkTercihler?.adet ?? 0;
const oncelikliKota = ilkNAdet > 0 ? ilkNAdet + ONCELIKLI_PAY : null;
const oncelikle = (havuz: AdayProgram[]) => {
const oncelikli = secenekler.oncelikli;
if (!oncelikli) return havuz;
const sirali = [...havuz].sort(
(a, b) =>
(a.katman ?? 0) - (b.katman ?? 0) ||
Number(oncelikli(b)) - Number(oncelikli(a)),
);
return oncelikliKota == null
? sirali
: oncelikliyeKotaAyir(sirali, oncelikli, oncelikliKota);
};
// Ekstralar katman 0 ile başa; ötekilerin gevşetme katmanı korunur
const ekstraIdKumesi = new Set(ekstra.map((p) => p.id));
const kirpVe = (havuz: AdayProgram[]) =>