v2 #10

Merged
bilal merged 34 commits from v2 into main 2026-10-11 01:38:50 +03:00
2 changed files with 122 additions and 6 deletions
Showing only changes of commit 62994d70d1 - Show all commits

View File

@@ -42,6 +42,7 @@ import { ILK_YIL } from "../veri-yillari";
import { import {
cikarSuzgeci, cikarSuzgeci,
efektifParams, efektifParams,
ilkTercihleriGarantile,
ilkTercihleriOneAl, ilkTercihleriOneAl,
kuralOzeti, kuralOzeti,
oncelikSuzgeci, oncelikSuzgeci,
@@ -325,6 +326,8 @@ export type RaporTani = {
uyariAtilan: string[]; uyariAtilan: string[];
/** Koddan eklenen uyarı sayısı. */ /** Koddan eklenen uyarı sayısı. */
uyariSablon: number; 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 // --- Deterministik satır metinleri: risk ve trend gerçek YÖK Atlas sıra
@@ -369,15 +372,22 @@ function mockRapor(
havuz: AdayProgram[], havuz: AdayProgram[],
params: RaporParams, params: RaporParams,
revizyonNotu?: string, revizyonNotu?: string,
oncelikli?: ((p: AdayProgram) => boolean) | null,
): RaporSonuc { ): RaporSonuc {
// Prod ile aynı hedef hesabı: mock, uyarlanmış dağılımı da birebir taklit eder // Prod ile aynı hedef hesabı: mock, uyarlanmış dağılımı da birebir taklit eder
const hedef = dilimHedefiHesapla(havuz, revizyonIskeleti(params)); const hedef = dilimHedefiHesapla(havuz, revizyonIskeleti(params));
const toplamHedef = hedef.hayal + hedef.dengeli + hedef.garanti; const toplamHedef = hedef.hayal + hedef.dengeli + hedef.garanti;
const efektif = (p: AdayProgram) => p.efektifSira ?? 0; 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) => const dilimden = (dilim: AdayProgram["dilim"], adet: number) =>
havuz havuz
.filter((p) => p.dilim === dilim) .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); .slice(0, adet);
const secilen = [ const secilen = [
@@ -386,7 +396,7 @@ function mockRapor(
...dilimden("garanti", hedef.garanti), ...dilimden("garanti", hedef.garanti),
]; ];
const tercihler = secilen.map((p, i) => ({ const tercihlerHam = secilen.map((p, i) => ({
programId: p.id, programId: p.id,
sira: i + 1, sira: i + 1,
dilim: p.dilim, dilim: p.dilim,
@@ -401,6 +411,9 @@ function mockRapor(
for (const p of secilen) { for (const p of secilen) {
programlar[p.id] = programBilgisi(p); 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 { return {
tercihler, tercihler,
@@ -551,7 +564,7 @@ export async function raporUretAyrintili(
// Dev'de API key yoksa gerçek Yapay Zeka yerine mock rapor (üretim gecikmesi taklidi) // Dev'de API key yoksa gerçek Yapay Zeka yerine mock rapor (üretim gecikmesi taklidi)
if (mockAktif()) { if (mockAktif()) {
await new Promise((r) => setTimeout(r, 1500)); 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 = [ const kullaniciMesaji = [
@@ -709,7 +722,31 @@ export async function raporUretAyrintili(
uyarilar: [], 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 // --- Metin denetimi (deterministik; ek model çağrısı yok). İhlalli metin
// kullanıcıya gitmez, koddan üretilen şablona düşer. // kullanıcıya gitmez, koddan üretilen şablona düşer.
@@ -882,6 +919,7 @@ export async function raporUretAyrintili(
genelSablon, genelSablon,
uyariAtilan, uyariAtilan,
uyariSablon, uyariSablon,
ilkNEklenen: garanti.eklenen,
}, },
}; };
} }

View File

@@ -8,14 +8,16 @@
* biner (`efektifParams`), "çıkar" havuza hiç girmez, "ilk N" ölçütü * biner (`efektifParams`), "çıkar" havuza hiç girmez, "ilk N" ölçütü
* ve "ekle" kimlikleri kırpmada öne alınır (rapor-havuzu.ts). * 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 * 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 * 3. Sıra: liste kurulduktan sonra "ilk N" ölçütüne uyanlar başa alınır
* (`ilkTercihleriOneAl`). * (`ilkTercihleriOneAl`).
* *
* Saf modül: DB'ye ve Next'e dokunmaz; sunucu ve betikler birlikte kullanır. * 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 { 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 { IDEAL_HEDEF, IDEAL_HEDEF_KOLAY_YERLESME, idealHedef } from "./rapor-havuzu";
import { kategoriEslesir } from "./kategoriler"; import { kategoriEslesir } from "./kategoriler";
import { uniturDevletMi } from "./unitur"; 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). */ /** 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 }; 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(); const tr = (s: string) => s.toLocaleLowerCase("tr-TR").trim();
function farkUygula(taban: readonly string[], fark: ListeFarki | undefined): string[] { 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)); 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 * 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 → * sırası korunur; `sira` yeniden numaralanır. Dilim sırası (hayal → dengeli →