From da8d303ea453cd5221e0afb152ad5439f35dccee Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Mon, 5 Oct 2026 08:49:41 +0300 Subject: [PATCH] =?UTF-8?q?fix(liste):=20ba=C5=9Fka=20programa=20ait=20ger?= =?UTF-8?q?ek=C3=A7e=20(il/=C3=BCniversite=20=C3=A7eli=C5=9Fkisi)=20=C5=9F?= =?UTF-8?q?ablona=20d=C3=BC=C5=9Fs=C3=BCn=20[KARAR]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kök neden 3 (04-eval-sonuclari): 888 gerekçenin 7'si başka programa aitti (ör. Erzurum'daki programa "sanayi şehri Konya'da"). - liste-denetim.ts: yerCeliskisi — veri tabanındaki il ve üniversite adlarından sözlük. Bulunma ekiyle ("Konya'da") anılan il programın ili değilse, ya da anılan il ne programın ne adayın seçtiği/oturduğu ilse, ya da anılan üniversite satırınki değilse çelişki. "İzmir'e yakın" (adayın ili), üniversite adındaki il, "Kıbrıs'ta" (KKTC şehirleri), İTÜ/ODTÜ gibi kısaltmalar ve bölge/ilçe adları ("Ege", "Çankaya") ihlal değil. - rapor.ts: çelişkili gerekçe koddan şablona düşer (ek model çağrısı yok). - /meraklisina şeması ve 4. bölüm anlatımı (AGENTS.md senkronu): yapay zekânın seçimi ve gerekçeleri kodla denetlenir; eksik satır havuzdan tamamlanır, vaat içeren ya da başka programı anlatan gerekçenin yerine standart açıklama yazılır; yapay zekâ zamanında yanıt vermezse liste aynı havuzdan kurallarla kurulur. Havuz dışına çıkamama kuralı değişmedi. Bu metin önceki iki commit'in (3128b01, 8462894) davranışını da kapsar. Kanıt: taban koşusunun 888 gerekçesi üzerinde çevrimdışı — 7 gerçek çelişkinin 7'si yakalandı, yanlış alarm 0 (eval'in 2 yanlış alarmı dahil). Canlı koşular: il çelişkisi 7 → 0 (iki koşuda 1 ve 0 gerekçe şablona düştü). Co-Authored-By: Claude Fable 5.1 --- .../pazarlama/components/huni-semasi.tsx | 4 +- .../components/meraklisina-icerik.tsx | 7 +- src/lib/ai/liste-denetim.ts | 127 ++++++++++++++++++ src/lib/ai/rapor.ts | 20 +++ 4 files changed, 155 insertions(+), 3 deletions(-) diff --git a/src/features/pazarlama/components/huni-semasi.tsx b/src/features/pazarlama/components/huni-semasi.tsx index fef00c5..c7f0f52 100644 --- a/src/features/pazarlama/components/huni-semasi.tsx +++ b/src/features/pazarlama/components/huni-semasi.tsx @@ -22,7 +22,7 @@ flowchart TD S -- "evet" --> G("Filtreler adım adım gevşetilir
önce danışmana söylediklerin, sonra alan, sonra il — listede açıkça belirtilir
seçimine uyan programlar havuzda kalır, boşluk sıralamana en yakın diğer programlarla dolar") G --> H H --> U("Havuz yine de dar kalırsa
liste eldeki programlarla kurulur, dağılım havuza uyarlanır ve sana açıkça söylenir") - S -- "hayır" --> Y("Yapay zekâ havuzdan en fazla 24 tercih seçer
havuzda olmayan bölümü yazamaz, her satıra gerekçe yazar") + S -- "hayır" --> Y("Yapay zekâ havuzdan en fazla 24 tercih seçer
havuzda olmayan bölümü yazamaz, her satıra gerekçe yazar
seçimi ve gerekçeleri kodla denetlenir — eksik kalan satır havuzdan tamamlanır, vaat içeren ya da başka programı anlatan gerekçenin yerine standart açıklama yazılır") U --> Y Y --> R("Risk notu ve trend özeti
gerçek sıralama geçmişinden hesaplanır, yapay zekâ yorumu değildir") R --> L("En fazla 24 tercihlik liste") @@ -45,7 +45,7 @@ export function HuniSemasi() { return ( Havuza yalnızca en son tercih kılavuzunda yer alan programlar girer; diff --git a/src/features/pazarlama/components/meraklisina-icerik.tsx b/src/features/pazarlama/components/meraklisina-icerik.tsx index 32fc4b5..4cdcf19 100644 --- a/src/features/pazarlama/components/meraklisina-icerik.tsx +++ b/src/features/pazarlama/components/meraklisina-icerik.tsx @@ -137,7 +137,12 @@ export function MeraklisinaIcerik() { açıkça belirtilerek nasıl kurulduğu da dahil. Bir filtre gevşediğinde seçimine uyan programlar havuzdan çıkmaz: önce onlar alınır, kalan boşluk sıralamana en yakın diğer programlarla - doldurulur. + doldurulur. Yapay zekânın seçtiği liste sana gelmeden önce kodla + denetlenir: havuzda olmayan ya da iki kez yazılan program atılır, + eksik kalan satır havuzdan tamamlanır; yerleşme vaadi içeren ya da + başka bir programı anlatan gerekçenin yerine standart bir açıklama + yazılır. Yapay zekâ zamanında yanıt vermezse liste aynı havuzdan + kurallarla kurulur ve bu, listenin başında sana söylenir.

diff --git a/src/lib/ai/liste-denetim.ts b/src/lib/ai/liste-denetim.ts index 8b6adab..26a07e6 100644 --- a/src/lib/ai/liste-denetim.ts +++ b/src/lib/ai/liste-denetim.ts @@ -106,6 +106,133 @@ export function yasakIfadeler(metin: string): string[] { return bulunan; } +// ───────────────────────── il / üniversite çelişkisi ───────────────────────── + +/** Aksan katlama (uzunluk korunur): "Izmir", "Eskisehir" yazımları da yakalansın. */ +const katla = (s: string) => + tr(s).replace(/[ışğüöçâîû]/g, (c) => ({ ı: "i", ş: "s", ğ: "g", ü: "u", ö: "o", ç: "c", â: "a", î: "i", û: "u" })[c] ?? c); + +const duzIfade = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + +const IL_KISA_AD: Record = { + afyon: "AFYONKARAHİSAR", + maraş: "KAHRAMANMARAŞ", + urfa: "ŞANLIURFA", + antep: "GAZİANTEP", + izmit: "KOCAELİ", + adapazarı: "SAKARYA", + antakya: "HATAY", +}; + +// Üniversite adının ayırt edici kısmı bölge, ilçe ya da gündelik sözcükse +// ("Ege'de", "başkent Ankara", "Çankaya'da", "Alanya'ya yakın") çelişki aranmaz. +const UNI_ANAHTAR_DISI = new Set( + ["ege", "marmara", "akdeniz", "trakya", "çukurova", "başkent", "kapadokya", "gazi", "kültür", "toros", "anadolu", "fırat", "dicle", "atılım", "doğuş", "ufuk", "bilgi", "ışık", "okan", "haliç", "maltepe", "üsküdar", "fenerbahçe", "galatasaray", "atatürk", "cumhuriyet", "yeni yüzyıl", "piri reis", "medeniyet", "sağlık bilimleri", "alanya", "mudanya", "tarsus", "pamukkale", "çankaya", "yaşar", "avrasya"].map(katla), +); + +const UNI_KISALTMA: Record = { + itü: "istanbul teknik", + odtü: "orta doğu teknik", + ytü: "yıldız teknik", + ktü: "karadeniz teknik", + gtü: "gebze teknik", + iyte: "izmir yüksek teknoloji", + deü: "dokuz eylül", +}; + +const KKTC_ILLERI = new Set(["LEFKOŞA", "GİRNE", "GAZİMAĞUSA", "GÜZELYURT", "LEFKE", "KIBRIS"]); + +const UNI_KISALTMA_KATLI = new Map(Object.entries(UNI_KISALTMA).map(([k, v]) => [katla(k), katla(v)])); + +export type YerSozlugu = { + /** katlanmış il adı (ve yaygın kısa ad) → veri tabanındaki il */ + iller: Map; + ilRe: RegExp; + /** katlanmış ayırt edici üniversite adı ("hacettepe", "gebze teknik") */ + uniler: Set; + uniRe: RegExp | null; +}; + +function uniAnahtari(universite: string): string { + return katla(universite) + .replace(/\s*\(.*?\)/g, "") + .replace(/\s+(?:universitesi|yuksek teknoloji enstitusu|meslek yuksekokulu)\b.*$/, (e) => + e.includes("yuksek teknoloji") ? " yuksek teknoloji" : "", + ) + .trim(); +} + +/** Bilinen il ve üniversite adlarından sözlük (veri tabanındaki tüm programlardan bir kez kurulur). */ +export function yerSozluguKur(satirlar: { il: string | null; universite: string }[]): YerSozlugu { + const iller = new Map(); + for (const s of satirlar) { + if (!s.il) continue; + // "BAKÜ-AZERBAYCAN", "SARAYBOSNA - BOSNA - HERSEK" → ilk parça + const ad = katla(s.il.split(/\s*-\s*/)[0]).trim(); + if (ad.length >= 3) iller.set(ad, s.il); + } + for (const [kisa, il] of Object.entries(IL_KISA_AD)) iller.set(katla(kisa), il); + const uniler = new Set(); + for (const s of satirlar) { + const a = uniAnahtari(s.universite); + if (a.length >= 5 && !iller.has(a) && !UNI_ANAHTAR_DISI.has(a)) uniler.add(a); + } + for (const [kisa, tam] of UNI_KISALTMA_KATLI) if (uniler.has(tam)) uniler.add(kisa); + const secenek = (k: Iterable) => [...k].sort((a, b) => b.length - a.length).map(duzIfade).join("|"); + return { + iller, + // Ek: 'da/'de/'ta/'te(ki) bulunma hâli ayrı yakalanır (programın yerini iddia eder) + ilRe: new RegExp(`(? { + const c = gerekce[i]; + return c != null && c !== c.toLocaleLowerCase("tr-TR"); + }; + for (const e of k.matchAll(sozluk.ilRe)) { + const i = e.index ?? 0; + // Özel ad: özgün metinde büyük harfle başlamalı ("ordu", "aydın", "ağrı", "van" sözcükleri il değildir) + if (!buyukHarfle(i)) continue; + const il = sozluk.iller.get(e[1])!; + if (il === program.il) continue; + // "Kıbrıs'ta": KKTC şehirlerindeki programlar için aynı yer + if (il === "KIBRIS" && program.il != null && KKTC_ILLERI.has(program.il)) continue; + if (uniK.includes(e[1]) || isimK.includes(e[1])) continue; + const bulunmaEki = Boolean(e[3]); + if (bulunmaEki || !adayIlleri.includes(il)) return `il: gerekçe ${il} diyor, program ${program.il ?? "?"}`; + } + if (sozluk.uniRe) { + for (const e of k.matchAll(sozluk.uniRe)) { + if (!buyukHarfle(e.index ?? 0)) continue; + const anahtar = UNI_KISALTMA_KATLI.get(e[1]) ?? e[1]; + if (!uniK.includes(anahtar)) return `üniversite: gerekçe "${e[1]}" diyor, program ${program.universite}`; + } + } + return null; +} + // ───────────────────────── sayı tutarlılığı ───────────────────────── /** diff --git a/src/lib/ai/rapor.ts b/src/lib/ai/rapor.ts index d11c52a..b7cc15a 100644 --- a/src/lib/ai/rapor.ts +++ b/src/lib/ai/rapor.ts @@ -13,7 +13,11 @@ import { dilimAdiniDuzelt, metindekiSayilar, yasakIfadeler, + yerCeliskisi, + yerSozluguKur, + type YerSozlugu, } from "./liste-denetim"; +import { getDb } from "../db"; import { kategoriEslesir } from "../kategoriler"; import { havuzOlustur, @@ -241,6 +245,18 @@ function secimiOnar( return { secim, onarim }; } +// --- Metin denetimi sözlüğü: bilinen il ve üniversite adları (tek sorgu, +// süreç boyunca önbellekte). +let yerSozluguOnbellek: YerSozlugu | null = null; +function yerSozlugu(): YerSozlugu { + yerSozluguOnbellek ??= yerSozluguKur( + getDb() + .prepare("SELECT DISTINCT il, universite FROM programs") + .all() as { il: string | null; universite: string }[], + ); + return yerSozluguOnbellek; +} + const GEREKCE_UST_SINIR = 200; // "Seçtiğin alanla eşleşiyor / alanına uygun": alan uyumu koddan bilinir @@ -633,6 +649,8 @@ export async function raporUretAyrintili( // --- Metin denetimi (deterministik; ek model çağrısı yok). İhlalli metin // kullanıcıya gitmez, koddan üretilen şablona düşer. + const sozluk = yerSozlugu(); + const adayIlleri = [...seciliIller, ...(profil?.evIl ? [profil.evIl] : [])]; // Adayın kendi söylediği sayılar ("300 km", "%50 burslu") metinde meşrudur const profilSayilari = metindekiSayilar(...profilSatir.map((s) => s.metin)); const sablonGerekceMetni = (p: AdayProgram): string => { @@ -660,6 +678,8 @@ export async function raporUretAyrintili( if (m.length > GEREKCE_UST_SINIR) return sablon("uzun"); const yasak = yasakIfadeler(m); if (yasak.length > 0) return sablon(`yasak ifade — ${yasak.join(" | ")}`); + const yer = yerCeliskisi(m, p, sozluk, adayIlleri); + if (yer) return sablon(`başka programa ait — ${yer}`); if (seciliKategoriler.length > 0 && !alanUyar(p) && ALAN_UYUMU_IDDIASI.test(m)) { return sablon("alan uyumu iddiası, program seçili alanda değil"); }