diff --git a/scripts/eval/revizyon-denetim.ts b/scripts/eval/revizyon-denetim.ts index de5f0d7..13f59e3 100644 --- a/scripts/eval/revizyon-denetim.ts +++ b/scripts/eval/revizyon-denetim.ts @@ -114,15 +114,24 @@ function eklenenEsit(r: Rapor, n: number): string[] { return r.tani?.ilkNEklenen === n ? [] : [`tani.ilkNEklenen ${r.tani?.ilkNEklenen} ≠ ${n}`]; } +/** İlk N'den sonraki satırların çoğunluğu sihirbaz illerinden mi ("ilk N" listenin kalanını kaydırmaz). */ +function kalanSihirbazdan(r: Rapor, adet: number, iller: string[]): string[] { + const kalan = r.rapor.tercihler.slice(adet); + const sihirbaz = kalan.filter((t) => iller.map(tr).includes(tr(r.rapor.programlar[t.programId]?.il))).length; + return sihirbaz * 2 > kalan.length ? [] : [`kalan ${kalan.length} satırın yalnız ${sihirbaz}'i sihirbaz illerinden (${iller.join("/")})`]; +} + const SENARYOLAR: Senaryo[] = [ { - // Bilal'in 7 Eki denemesi. Not: İstanbul her dilimde 20+ programla havuz - // kırpmasının öncelikli kotasını tek başına doldurur → havuz 60/60 İstanbul - // olur, sahte uç öncelikli olmayan satır bulamaz, takas gerekmez - // (ilkNEklenen 0). Garantinin takas yolu "ilk5-takas" senaryosunda ölçülür. + // Bilal'in 7 Eki denemesi. İstanbul her dilimde 20+ programla havuzu tek + // başına doldurabilirdi (eskiden 60/60 İstanbul → 24/24 İstanbul); şimdi + // kırpma öncelikliye yalnız N + pay yer ayırır (rapor-havuzu.ts + // oncelikliyeKotaAyir), kalan havuz Ankara/İzmir'den kurulur. Sahte uç + // İstanbul'u atlar, kod 5'ini takaslar; kalan 19'un çoğunluğu sihirbaz + // illerinden olmalı ("ilk N yalnız ilk N'yi belirler" ürün kararı). ad: "ilk5-istanbul", params: taban({ ilkTercihler: { adet: 5, iller: ["İSTANBUL"] }, not: "ilk 5 İstanbul" }), - dogrula: (r) => ilkNIl(r, 5, "İSTANBUL"), + dogrula: (r) => [...ilkNIl(r, 5, "İSTANBUL"), ...eklenenEsit(r, 5), ...kalanSihirbazdan(r, 5, ["ANKARA", "İZMİR"])], }, { // Takas yolu: Yalova 100.000 say'da havuza 0/3/8 (hayal/dengeli/güvenli) diff --git a/src/lib/rapor-havuzu.ts b/src/lib/rapor-havuzu.ts index c4d7ad7..5bb6266 100644 --- a/src/lib/rapor-havuzu.ts +++ b/src/lib/rapor-havuzu.ts @@ -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 = { hayal: [], dengeli: [], garanti: [] }; + const siradan: Record = { 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[]) =>