fix(revizyon): "ilk N" ölçütü seçimde kodla garanti ediliyor; mock yolu da kuralı uyguluyor
Model 24 geçerli satırı öncelikli satırsız doldurunca secimiOnar tamamlayacak
boşluk bulamıyor, ilkTercihleriOneAl öne alacak satır bulamıyordu ("ilk 5
İstanbul" istendi, liste değişmedi — 7 Eki 2026). Yeni saf fonksiyon
ilkTercihleriGarantile: ölçüte uyan ama seçilmemiş havuz programları, aynı
dilimdeki uymayan satırların (önce kodla gelen, sonra en düşük uyumlu; model
gerekçeli satırlar en son) yerine takasla girer, dilim adetleri değişmez.
rapor.ts: nihai seçimden sonra uygulanır, tani.ilkNEklenen alanı eklendi,
onarim.tamamlanan takas kadar artar, modelSecimi nihai seçimden yeniden
sayılır. Dev mock dilim içinde öncelikliyi önce alır ve ilkTercihleriOneAl
uygular.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -42,6 +42,7 @@ import { ILK_YIL } from "../veri-yillari";
|
||||
import {
|
||||
cikarSuzgeci,
|
||||
efektifParams,
|
||||
ilkTercihleriGarantile,
|
||||
ilkTercihleriOneAl,
|
||||
kuralOzeti,
|
||||
oncelikSuzgeci,
|
||||
@@ -325,6 +326,8 @@ export type RaporTani = {
|
||||
uyariAtilan: string[];
|
||||
/** Koddan eklenen uyarı sayısı. */
|
||||
uyariSablon: number;
|
||||
/** Revizyonun "ilk N" ölçütü için kodla takas edilen satır sayısı (kural yoksa 0). */
|
||||
ilkNEklenen: number;
|
||||
};
|
||||
|
||||
// --- Deterministik satır metinleri: risk ve trend gerçek YÖK Atlas sıra
|
||||
@@ -369,15 +372,22 @@ function mockRapor(
|
||||
havuz: AdayProgram[],
|
||||
params: RaporParams,
|
||||
revizyonNotu?: string,
|
||||
oncelikli?: ((p: AdayProgram) => boolean) | null,
|
||||
): RaporSonuc {
|
||||
// Prod ile aynı hedef hesabı: mock, uyarlanmış dağılımı da birebir taklit eder
|
||||
const hedef = dilimHedefiHesapla(havuz, revizyonIskeleti(params));
|
||||
const toplamHedef = hedef.hayal + hedef.dengeli + hedef.garanti;
|
||||
const efektif = (p: AdayProgram) => p.efektifSira ?? 0;
|
||||
// Revizyonun "ilk N" ölçütüne uyanlar dilim içinde önce gelir (prod'daki
|
||||
// garanti takasının mock karşılığı); eşitte taban sırası iyiden kötüye
|
||||
const dilimden = (dilim: AdayProgram["dilim"], adet: number) =>
|
||||
havuz
|
||||
.filter((p) => p.dilim === dilim)
|
||||
.sort((a, b) => efektif(a) - efektif(b))
|
||||
.sort(
|
||||
(a, b) =>
|
||||
Number(oncelikli?.(b) ?? false) - Number(oncelikli?.(a) ?? false) ||
|
||||
efektif(a) - efektif(b),
|
||||
)
|
||||
.slice(0, adet);
|
||||
|
||||
const secilen = [
|
||||
@@ -386,7 +396,7 @@ function mockRapor(
|
||||
...dilimden("garanti", hedef.garanti),
|
||||
];
|
||||
|
||||
const tercihler = secilen.map((p, i) => ({
|
||||
const tercihlerHam = secilen.map((p, i) => ({
|
||||
programId: p.id,
|
||||
sira: i + 1,
|
||||
dilim: p.dilim,
|
||||
@@ -401,6 +411,9 @@ function mockRapor(
|
||||
for (const p of secilen) {
|
||||
programlar[p.id] = programBilgisi(p);
|
||||
}
|
||||
// Prod sırasıyla aynı: "ilk N" ölçütüne uyanlar başa (revizyon-kurali.ts)
|
||||
const secilenMap = new Map(secilen.map((p) => [p.id, p]));
|
||||
const { tercihler } = ilkTercihleriOneAl(tercihlerHam, params.revizyon, (id) => secilenMap.get(id));
|
||||
|
||||
return {
|
||||
tercihler,
|
||||
@@ -551,7 +564,7 @@ export async function raporUretAyrintili(
|
||||
// Dev'de API key yoksa gerçek Yapay Zeka yerine mock rapor (üretim gecikmesi taklidi)
|
||||
if (mockAktif()) {
|
||||
await new Promise((r) => setTimeout(r, 1500));
|
||||
return { rapor: mockRapor(havuz, params, revizyonNotu), tani: null };
|
||||
return { rapor: mockRapor(havuz, params, revizyonNotu, oncelikli), tani: null };
|
||||
}
|
||||
|
||||
const kullaniciMesaji = [
|
||||
@@ -709,7 +722,31 @@ export async function raporUretAyrintili(
|
||||
uyarilar: [],
|
||||
};
|
||||
}
|
||||
const nihai = enIyi;
|
||||
// "İlk N" garantisi (revizyon-kurali.ts): model öncelikli satırları
|
||||
// atlamışsa ölçüte uyan havuz programları, aynı dilimdeki uymayan satırların
|
||||
// yerine takasla girer; dilim adetleri değişmez. Prompt'taki "EN AZ N tane
|
||||
// seç" ricadır, sonuç buraya bağlıdır. Takasla giren satırın gerekçesi
|
||||
// şablondan basılır (kaynak "tamamlandi").
|
||||
const garanti = ilkTercihleriGarantile(
|
||||
enIyi.secim,
|
||||
havuz,
|
||||
kural,
|
||||
uyum,
|
||||
(p): SeciliSatir => ({ programId: p.id, gerekce: null, kaynak: "tamamlandi" }),
|
||||
);
|
||||
const nihai: Deneme = {
|
||||
...enIyi,
|
||||
secim: garanti.secim,
|
||||
onarim: {
|
||||
...enIyi.onarim,
|
||||
tamamlanan: enIyi.onarim.tamamlanan + garanti.eklenen,
|
||||
// Takas model satırı çıkarmış olabilir: sayı nihai seçimden yeniden okunur
|
||||
modelSecimi: DILIMLER.reduce(
|
||||
(n, d) => n + garanti.secim[d].filter((s) => s.kaynak !== "tamamlandi").length,
|
||||
0,
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
// --- Metin denetimi (deterministik; ek model çağrısı yok). İhlalli metin
|
||||
// kullanıcıya gitmez, koddan üretilen şablona düşer.
|
||||
@@ -882,6 +919,7 @@ export async function raporUretAyrintili(
|
||||
genelSablon,
|
||||
uyariAtilan,
|
||||
uyariSablon,
|
||||
ilkNEklenen: garanti.eklenen,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,14 +8,16 @@
|
||||
* biner (`efektifParams`), "çıkar" havuza hiç girmez, "ilk N" ölçütü
|
||||
* ve "ekle" kimlikleri kırpmada öne alınır (rapor-havuzu.ts).
|
||||
* 2. Seçim: iskelet dilim hedefini değiştirir; tamamlama/kırpma ölçüte
|
||||
* uyanı önceler (lib/ai/rapor.ts).
|
||||
* uyanı önceler (lib/ai/rapor.ts). Model ölçüte uyan satırı seçmemişse
|
||||
* kod takasla garantiler (`ilkTercihleriGarantile`) — prompt'taki
|
||||
* "EN AZ N tane seç" ricadır, sonuç koda bağlıdır.
|
||||
* 3. Sıra: liste kurulduktan sonra "ilk N" ölçütüne uyanlar başa alınır
|
||||
* (`ilkTercihleriOneAl`).
|
||||
*
|
||||
* Saf modül: DB'ye ve Next'e dokunmaz; sunucu ve betikler birlikte kullanır.
|
||||
*/
|
||||
import type { RaporParams, RaporTercih, RevizyonKurali, ListeFarki } from "@/features/rapor/types/rapor";
|
||||
import type { AdayProgram, DilimHedef } from "./rapor-havuzu";
|
||||
import type { AdayProgram, Dilim, DilimHedef } from "./rapor-havuzu";
|
||||
import { IDEAL_HEDEF, IDEAL_HEDEF_KOLAY_YERLESME, idealHedef } from "./rapor-havuzu";
|
||||
import { kategoriEslesir } from "./kategoriler";
|
||||
import { uniturDevletMi } from "./unitur";
|
||||
@@ -24,6 +26,8 @@ import type { UniturGrubu } from "@/types/yokatlas";
|
||||
/** Hayal ağırlıklı iskelet: 7 hayal / 12 dengeli / 5 güvenli (toplam 24). */
|
||||
export const IDEAL_HEDEF_HAYAL_AGIRLIKLI: DilimHedef = { hayal: 7, dengeli: 12, garanti: 5 };
|
||||
|
||||
const DILIMLER: Dilim[] = ["hayal", "dengeli", "garanti"];
|
||||
|
||||
const tr = (s: string) => s.toLocaleLowerCase("tr-TR").trim();
|
||||
|
||||
function farkUygula(taban: readonly string[], fark: ListeFarki | undefined): string[] {
|
||||
@@ -141,6 +145,80 @@ export function cikarSuzgeci(k: RevizyonKurali | undefined): ((p: AdayProgram) =
|
||||
bolumler.some((b) => tr(p.isim).includes(b));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dilim seçimi kurulduktan sonra "ilk N" ölçütüne uyan satır sayısı N'den
|
||||
* azsa, havuzdaki uyan ama seçilmemiş programlar, aynı dilimdeki en düşük
|
||||
* uyumlu uymayan satırların yerine konur (dilim adetleri değişmez). Havuzda
|
||||
* yeterli uyan yoksa eldeki kadarı; takas edilen satır `yeniSatir` ile
|
||||
* kurulur (rapor.ts kaynak "tamamlandi" verir, gerekçe şablondan basılır).
|
||||
* Dönen `eklenen` = takas sayısı.
|
||||
*
|
||||
* Takas kuralı: öncelikli adaylar `uyum` azalan sırayla gelir; her aday kendi
|
||||
* dilimindeki uymayan satırlar arasından `uyum`u en düşük olanı çıkarır,
|
||||
* modelin gerekçe yazdığı satırlar (`kaynak === "model"`) en son çıkar. Dilim
|
||||
* hedefi 0 olan (seçimi boş) dilimde takas yapılmaz. Kök neden (7 Eki 2026):
|
||||
* model 24 geçerli satırı öncelikli satırsız doldurunca `secimiOnar`
|
||||
* tamamlayacak boşluk bulamıyor, `ilkTercihleriOneAl` öne alacak satır
|
||||
* bulamıyordu; "ilk 5 İstanbul" istendi, liste değişmedi gibi görünüyordu.
|
||||
*/
|
||||
export function ilkTercihleriGarantile<S extends { programId: string; kaynak: string }>(
|
||||
secim: Record<Dilim, S[]>,
|
||||
havuz: readonly AdayProgram[],
|
||||
k: RevizyonKurali | undefined,
|
||||
uyum: (p: AdayProgram) => number,
|
||||
yeniSatir: (p: AdayProgram) => S,
|
||||
): { secim: Record<Dilim, S[]>; eklenen: number } {
|
||||
const o = k?.ilkTercihler;
|
||||
if (!o || olcutBos(o) || o.adet < 1) return { secim, eklenen: 0 };
|
||||
const havuzMap = new Map(havuz.map((p) => [p.id, p]));
|
||||
const uyar = (id: string) => {
|
||||
const p = havuzMap.get(id);
|
||||
return p != null && olcuteUyar(p, o);
|
||||
};
|
||||
const sonuc: Record<Dilim, S[]> = {
|
||||
hayal: [...secim.hayal],
|
||||
dengeli: [...secim.dengeli],
|
||||
garanti: [...secim.garanti],
|
||||
};
|
||||
const secili = new Set<string>();
|
||||
let uyan = 0;
|
||||
for (const d of DILIMLER) {
|
||||
for (const s of sonuc[d]) {
|
||||
secili.add(s.programId);
|
||||
if (uyar(s.programId)) uyan++;
|
||||
}
|
||||
}
|
||||
let eksik = o.adet - uyan;
|
||||
if (eksik <= 0) return { secim, eklenen: 0 };
|
||||
// Array.prototype.sort kararlıdır: eşit uyumda havuz sırası (yakınlık) korunur
|
||||
const adaylar = havuz
|
||||
.filter((p) => !secili.has(p.id) && olcuteUyar(p, o))
|
||||
.sort((a, b) => uyum(b) - uyum(a));
|
||||
let eklenen = 0;
|
||||
for (const p of adaylar) {
|
||||
if (eksik <= 0) break;
|
||||
const satirlar = sonuc[p.dilim];
|
||||
// Çıkarılacak satır: uymayanlar arasında önce kodla gelenler, sonra en düşük uyum
|
||||
let cikacak = -1;
|
||||
let cikacakAnahtar = Number.POSITIVE_INFINITY;
|
||||
for (let i = 0; i < satirlar.length; i++) {
|
||||
const s = satirlar[i];
|
||||
if (uyar(s.programId)) continue;
|
||||
const sp = havuzMap.get(s.programId);
|
||||
const anahtar = (s.kaynak === "model" ? 1_000 : 0) + (sp ? uyum(sp) : 0);
|
||||
if (anahtar < cikacakAnahtar) {
|
||||
cikacakAnahtar = anahtar;
|
||||
cikacak = i;
|
||||
}
|
||||
}
|
||||
if (cikacak < 0) continue;
|
||||
satirlar[cikacak] = yeniSatir(p);
|
||||
eklenen++;
|
||||
eksik--;
|
||||
}
|
||||
return { secim: sonuc, eklenen };
|
||||
}
|
||||
|
||||
/**
|
||||
* Kurulmuş listede "ilk N" ölçütüne uyan ilk N satırı başa alır, kalanların
|
||||
* sırası korunur; `sira` yeniden numaralanır. Dilim sırası (hayal → dengeli →
|
||||
|
||||
Reference in New Issue
Block a user