fix(havuz): B3 — alan süzgeci kırpmadan önce uygulansın [KARAR]

Havuz her dilimden sıraya en yakın 60 programı alıp alan (kategori)
süzgecini ondan sonra uyguluyordu; il seçmeyen adayın alanı, o alanda
onlarca program varken "yeterli program yok" diye gevşiyordu (matris B3:
318 denemenin 206'sı). Artık dilimin tamamı okunur (db.havuzAdaylari),
il + alan süzgeci ona uygulanır, 24 eşiği gerçek sayıya bakar, kırpma en
sonda yapılır. Gevşetme sırası ve kırpma kuralı değişmedi.

Kanıt (önce → sonra, havuzda seçili alanla eşleşen): a16 EA 200.000
Turizm 1/60 → 33/33; p03 8/60 → 60/60; a08 5/60 → 60/60; hiçbiri artık
alan gevşetmiyor.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-05 01:13:23 +03:00
parent b26df27fcd
commit ef68f93463
2 changed files with 44 additions and 24 deletions

View File

@@ -239,6 +239,28 @@ export function searchByRank(
}; };
} }
/**
* Aday havuzunun ham girdisi: üç dilimin TAMAMI, yakından uzağa sıralı
* (limit yok). Alan (kategori) DB'de kolon değil, JS'te isim+fakülteden
* türüyor; süzgeç limitten SONRA uygulanınca sıraya en yakın satırlar her
* alandan karışık geldiği için seçilen alan olduğundan az görünüyordu. Bu
* yüzden havuz dilimin tamamını alır, alan/il süzgecini kendisi uygular ve
* en son kırpar (bkz. lib/rapor-havuzu.ts). En büyük puan türü ~9 bin satır.
*/
export function havuzAdaylari(
sira: number,
turKey: PuanTuruKey,
opts: { uniturGrubu?: UniturGrubu; ozellik?: OzellikSuzgeci } = {}
): Record<DilimKey, Program[]> {
// LIMIT -1: SQLite'ta "sınırsız"
const ortak = { ...opts, limit: -1 };
return {
hayal: dilimAra(sira, turKey, "hayal", ortak).programlar,
dengeli: dilimAra(sira, turKey, "dengeli", ortak).programlar,
garanti: dilimAra(sira, turKey, "garanti", ortak).programlar,
};
}
/** /**
* Programa yerleşen son kişinin yıl bazlı netleri, yeniden eskiye sıralı. * Programa yerleşen son kişinin yıl bazlı netleri, yeniden eskiye sıralı.
* Veri scripts/detay.ts ile YÖK Atlas /api/netler/search'ten doldurulur. * Veri scripts/detay.ts ile YÖK Atlas /api/netler/search'ten doldurulur.

View File

@@ -1,6 +1,6 @@
// server-only guard'ı bilinçli yok: scripts/tadimlik-uret.ts bu modülü düz // 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. // node (tsx) altında import ediyor; server-only paketi orada fırlar.
import { searchByRank, type OzellikSuzgeci } from "./db"; import { havuzAdaylari, type OzellikSuzgeci } from "./db";
import type { DanismanProfili } from "./danisman-profil"; import type { DanismanProfili } from "./danisman-profil";
import { yakinIller } from "./mesafe"; import { yakinIller } from "./mesafe";
import { import {
@@ -70,7 +70,6 @@ export function profilSuzgeci(
} }
const HEDEF = 24; const HEDEF = 24;
const LIMIT = 60;
export type DilimHedef = Record<Dilim, number>; export type DilimHedef = Record<Dilim, number>;
@@ -111,8 +110,8 @@ export function dilimHedefiHesapla(havuz: AdayProgram[]): DilimHedef {
} }
return hedef; return hedef;
} }
// Prompt'a giden nihai havuz: dilim başına aday sayısı. Arama LIMIT ile geniş // Prompt'a giden nihai havuz: dilim başına aday sayısı. Arama dilimin tamamını
// yapılır (filtre/gevşetme zenginliği için), filtre sonrası buraya kırpılır — // alır (filtre/gevşetme gerçek sayıya baksın diye), filtre sonrası buraya kırpılır —
// 20×3=60 program ≈ 8k token; 180 program 24k token tutuyor ve lokal/ücretsiz // 20×3=60 program ≈ 8k token; 180 program 24k token tutuyor ve lokal/ücretsiz
// modellerde prefill + isabet maliyeti yüksek (ölçüm: 6 Ağu 2026). // modellerde prefill + isabet maliyeti yüksek (ölçüm: 6 Ağu 2026).
const PROMPT_LIMIT_DILIM = 20; const PROMPT_LIMIT_DILIM = 20;
@@ -141,6 +140,9 @@ function dilimEkle(r: {
* profiline (dil, öğretim türü, burs, ücret, km) göre filtreler; havuz 24'ün * 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 * 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. * liste her zaman kurulabilsin. /meraklisina şeması bu sırayı anlatı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.
*/ */
export function havuzOlustur( export function havuzOlustur(
params: RaporParams, params: RaporParams,
@@ -160,6 +162,10 @@ export function havuzOlustur(
kategoriler.length > 0 kategoriler.length > 0
? havuz.filter((p) => kategoriEslesir(p.isim, kategoriler, p.fakulte)) ? havuz.filter((p) => kategoriEslesir(p.isim, kategoriler, p.fakulte))
: havuz; : havuz;
const ilFiltre = (havuz: AdayProgram[], liste: string[] | undefined) =>
liste && liste.length > 0
? havuz.filter((p) => p.il != null && liste.includes(p.il))
: havuz;
// 0. Profil süzgeci (danışmana söylenenler) — yalnız yeterli havuz bırakırsa. // 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; // Bırakmazsa profil gevşetilir ve aşağıdaki sihirbaz akışı aynen sürer;
@@ -167,13 +173,14 @@ export function havuzOlustur(
const suzgec = profilSuzgeci(profil, iller.length > 0); const suzgec = profilSuzgeci(profil, iller.length > 0);
const profilGevsetildi: Gevsetilen[] = []; const profilGevsetildi: Gevsetilen[] = [];
if (suzgec.ozellik || suzgec.iller) { if (suzgec.ozellik || suzgec.iller) {
const profilliHam = dilimEkle( const profilliHam = ilFiltre(
searchByRank(params.sira, params.tur, { dilimEkle(
iller: iller.length > 0 ? iller : suzgec.iller, havuzAdaylari(params.sira, params.tur, {
uniturGrubu, uniturGrubu,
ozellik: suzgec.ozellik, ozellik: suzgec.ozellik,
limitPerBucket: LIMIT,
}), }),
),
iller.length > 0 ? iller : suzgec.iller,
); );
const profilliFiltreli = kategoriFiltre(profilliHam); const profilliFiltreli = kategoriFiltre(profilliHam);
if (profilliFiltreli.length >= HEDEF) { if (profilliFiltreli.length >= HEDEF) {
@@ -185,14 +192,11 @@ export function havuzOlustur(
profilGevsetildi.push("profil"); profilGevsetildi.push("profil");
} }
// 1. Tam filtre: iller + üniversite tipi (SQL), kategori (JS) // 1. Tam filtre: üniversite tipi (SQL), iller + kategori (JS)
const tamHam = dilimEkle( const ilsizHam = dilimEkle(
searchByRank(params.sira, params.tur, { havuzAdaylari(params.sira, params.tur, { uniturGrubu }),
iller: iller.length > 0 ? iller : undefined,
uniturGrubu,
limitPerBucket: LIMIT,
}),
); );
const tamHam = ilFiltre(ilsizHam, iller);
const tamFiltreli = kategoriFiltre(tamHam); const tamFiltreli = kategoriFiltre(tamHam);
if (tamFiltreli.length >= HEDEF) { if (tamFiltreli.length >= HEDEF) {
return { return {
@@ -212,12 +216,6 @@ export function havuzOlustur(
// 3. İl gevşet: il filtresini kaldırıp kategoriye geri dön // 3. İl gevşet: il filtresini kaldırıp kategoriye geri dön
const gevsetildi: Gevsetilen[] = [...profilGevsetildi]; const gevsetildi: Gevsetilen[] = [...profilGevsetildi];
if (iller.length > 0) gevsetildi.push("il"); if (iller.length > 0) gevsetildi.push("il");
const ilsizHam = dilimEkle(
searchByRank(params.sira, params.tur, {
uniturGrubu,
limitPerBucket: LIMIT,
}),
);
const ilsizFiltreli = kategoriFiltre(ilsizHam); const ilsizFiltreli = kategoriFiltre(ilsizHam);
if (ilsizFiltreli.length >= HEDEF) { if (ilsizFiltreli.length >= HEDEF) {
return { return {