From 62994d70d1e87f938dac282ffab26e47330d4b22 Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Wed, 7 Oct 2026 23:23:21 +0300 Subject: [PATCH] =?UTF-8?q?fix(revizyon):=20"ilk=20N"=20=C3=B6l=C3=A7?= =?UTF-8?q?=C3=BCt=C3=BC=20se=C3=A7imde=20kodla=20garanti=20ediliyor;=20mo?= =?UTF-8?q?ck=20yolu=20da=20kural=C4=B1=20uyguluyor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/lib/ai/rapor.ts | 46 +++++++++++++++++++-- src/lib/revizyon-kurali.ts | 82 +++++++++++++++++++++++++++++++++++++- 2 files changed, 122 insertions(+), 6 deletions(-) diff --git a/src/lib/ai/rapor.ts b/src/lib/ai/rapor.ts index b3a6fce..a68b78b 100644 --- a/src/lib/ai/rapor.ts +++ b/src/lib/ai/rapor.ts @@ -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, }, }; } diff --git a/src/lib/revizyon-kurali.ts b/src/lib/revizyon-kurali.ts index 5a9b2d0..db77b0f 100644 --- a/src/lib/revizyon-kurali.ts +++ b/src/lib/revizyon-kurali.ts @@ -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( + secim: Record, + havuz: readonly AdayProgram[], + k: RevizyonKurali | undefined, + uyum: (p: AdayProgram) => number, + yeniSatir: (p: AdayProgram) => S, +): { secim: Record; 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 = { + hayal: [...secim.hayal], + dengeli: [...secim.dengeli], + garanti: [...secim.garanti], + }; + const secili = new Set(); + 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 →