v2 #10
@@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 →
|
||||||
|
|||||||
Reference in New Issue
Block a user