fix(liste): başka programa ait gerekçe (il/üniversite çelişkisi) şablona düşsün [KARAR]
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 <noreply@anthropic.com>
This commit is contained in:
@@ -22,7 +22,7 @@ flowchart TD
|
|||||||
S -- "evet" --> G("Filtreler adım adım gevşetilir<br/>önce danışmana söylediklerin, sonra alan, sonra il — listede açıkça belirtilir<br/>seçimine uyan programlar havuzda kalır, boşluk sıralamana en yakın diğer programlarla dolar")
|
S -- "evet" --> G("Filtreler adım adım gevşetilir<br/>önce danışmana söylediklerin, sonra alan, sonra il — listede açıkça belirtilir<br/>seçimine uyan programlar havuzda kalır, boşluk sıralamana en yakın diğer programlarla dolar")
|
||||||
G --> H
|
G --> H
|
||||||
H --> U("Havuz yine de dar kalırsa<br/>liste eldeki programlarla kurulur, dağılım havuza uyarlanır ve sana açıkça söylenir")
|
H --> U("Havuz yine de dar kalırsa<br/>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<br/>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<br/>havuzda olmayan bölümü yazamaz, her satıra gerekçe yazar<br/>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
|
U --> Y
|
||||||
Y --> R("Risk notu ve trend özeti<br/>gerçek sıralama geçmişinden hesaplanır, yapay zekâ yorumu değildir")
|
Y --> R("Risk notu ve trend özeti<br/>gerçek sıralama geçmişinden hesaplanır, yapay zekâ yorumu değildir")
|
||||||
R --> L("En fazla 24 tercihlik liste")
|
R --> L("En fazla 24 tercihlik liste")
|
||||||
@@ -45,7 +45,7 @@ export function HuniSemasi() {
|
|||||||
return (
|
return (
|
||||||
<ElCizimiSema
|
<ElCizimiSema
|
||||||
tanim={SEMA}
|
tanim={SEMA}
|
||||||
ariaLabel="Sihirbaz akış şeması: sıralamanla açılan programlar alan, il ve devlet/vakıf adımlarıyla, danışmana söyleyip kaydettiğin tercihler (öğretim dili, uzaktan/örgün, burs, yıllık ücret sınırı, evden uzaklık) varsa onlarla da aday havuzuna daraltılır; havuza yalnızca en son tercih kılavuzunda yer alan programlar girer, kapanan ya da kodu değişip eski kaydı kalan programlar önerilmez, yalnızca KKTC uyruklulara ya da meslek lisesi mezunlarına ayrılmış kontenjanlar da havuza girmez, sıralaman bir bölümün başarı sırası barajının gerisindeyse (tıp, hukuk, mühendislik, öğretmenlik gibi) o bölüm de girmez; alanlar bölümün adına ve bağlı olduğu fakülteye göre belirlenir, il adımında programı olan tüm iller listelenir; havuz 24'ün altına düşerse filtreler önce danışmana söylediklerin, sonra alan, sonra il sırasıyla gevşetilir, gevşeyen seçimine uyan programlar havuzda kalır ve boşluk sıralamana en yakın diğer programlarla dolar, yine de dar kalırsa liste eldeki programlarla kurulur ve dağılım havuza uyarlanır; yapay zekâ havuzdan üstte hayal, ortada dengeli, sonda güvenli olacak şekilde en fazla 24 tercih seçip her satıra gerekçe yazar; risk notu ve trend özeti gerçek sıralama geçmişinden hesaplanır."
|
ariaLabel="Sihirbaz akış şeması: sıralamanla açılan programlar alan, il ve devlet/vakıf adımlarıyla, danışmana söyleyip kaydettiğin tercihler (öğretim dili, uzaktan/örgün, burs, yıllık ücret sınırı, evden uzaklık) varsa onlarla da aday havuzuna daraltılır; havuza yalnızca en son tercih kılavuzunda yer alan programlar girer, kapanan ya da kodu değişip eski kaydı kalan programlar önerilmez, yalnızca KKTC uyruklulara ya da meslek lisesi mezunlarına ayrılmış kontenjanlar da havuza girmez, sıralaman bir bölümün başarı sırası barajının gerisindeyse (tıp, hukuk, mühendislik, öğretmenlik gibi) o bölüm de girmez; alanlar bölümün adına ve bağlı olduğu fakülteye göre belirlenir, il adımında programı olan tüm iller listelenir; havuz 24'ün altına düşerse filtreler önce danışmana söylediklerin, sonra alan, sonra il sırasıyla gevşetilir, gevşeyen seçimine uyan programlar havuzda kalır ve boşluk sıralamana en yakın diğer programlarla dolar, yine de dar kalırsa liste eldeki programlarla kurulur ve dağılım havuza uyarlanır; yapay zekâ havuzdan üstte hayal, ortada dengeli, sonda güvenli olacak şekilde en fazla 24 tercih seçip 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; risk notu ve trend özeti gerçek sıralama geçmişinden hesaplanır."
|
||||||
figcaption={
|
figcaption={
|
||||||
<>
|
<>
|
||||||
Havuza yalnızca en son tercih kılavuzunda yer alan programlar girer;
|
Havuza yalnızca en son tercih kılavuzunda yer alan programlar girer;
|
||||||
|
|||||||
@@ -137,7 +137,12 @@ export function MeraklisinaIcerik() {
|
|||||||
açıkça belirtilerek nasıl kurulduğu da dahil. Bir filtre
|
açıkça belirtilerek nasıl kurulduğu da dahil. Bir filtre
|
||||||
gevşediğinde seçimine uyan programlar havuzdan çıkmaz: önce onlar
|
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
|
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.
|
||||||
</p>
|
</p>
|
||||||
<HuniSemasi />
|
<HuniSemasi />
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -106,6 +106,133 @@ export function yasakIfadeler(metin: string): string[] {
|
|||||||
return bulunan;
|
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<string, string> = {
|
||||||
|
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<string, string> = {
|
||||||
|
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<string, string>;
|
||||||
|
ilRe: RegExp;
|
||||||
|
/** katlanmış ayırt edici üniversite adı ("hacettepe", "gebze teknik") */
|
||||||
|
uniler: Set<string>;
|
||||||
|
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<string, string>();
|
||||||
|
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<string>();
|
||||||
|
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<string>) => [...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(`(?<![\\p{L}])(${secenek(iller.keys())})(?:(['’]?)([dt][ae](?:ki)?))?(?![\\p{L}])`, "gu"),
|
||||||
|
uniler,
|
||||||
|
uniRe: uniler.size ? new RegExp(`(?<![\\p{L}])(${secenek(uniler)})(?![\\p{L}])`, "gu") : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gerekçe, satırın programıyla çelişen bir il ya da üniversite adı anıyorsa
|
||||||
|
* neden metni döner (yoksa null). "Gerekçe başka programa ait" hatasının
|
||||||
|
* (ölçüm: 7/888 satır; ör. Erzurum'daki programa "Konya'da") kod denetimi.
|
||||||
|
*
|
||||||
|
* İl: (a) bulunma ekiyle ("Konya'da") anılan il programın ili değilse çelişki;
|
||||||
|
* (b) eksiz anılan il ne programın ili, ne adayın seçtiği/oturduğu il ise
|
||||||
|
* çelişki ("İzmir'e yakın" adayın kendi iliyse meşrudur).
|
||||||
|
* Üniversite: anılan üniversite satırınki değilse çelişki.
|
||||||
|
*/
|
||||||
|
export function yerCeliskisi(
|
||||||
|
gerekce: string,
|
||||||
|
program: { isim: string; universite: string; il: string | null },
|
||||||
|
sozluk: YerSozlugu,
|
||||||
|
adayIlleri: readonly string[] = [],
|
||||||
|
): string | null {
|
||||||
|
const k = katla(gerekce);
|
||||||
|
const uniK = katla(program.universite);
|
||||||
|
const isimK = katla(program.isim);
|
||||||
|
const buyukHarfle = (i: number) => {
|
||||||
|
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ığı ─────────────────────────
|
// ───────────────────────── sayı tutarlılığı ─────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ import {
|
|||||||
dilimAdiniDuzelt,
|
dilimAdiniDuzelt,
|
||||||
metindekiSayilar,
|
metindekiSayilar,
|
||||||
yasakIfadeler,
|
yasakIfadeler,
|
||||||
|
yerCeliskisi,
|
||||||
|
yerSozluguKur,
|
||||||
|
type YerSozlugu,
|
||||||
} from "./liste-denetim";
|
} from "./liste-denetim";
|
||||||
|
import { getDb } from "../db";
|
||||||
import { kategoriEslesir } from "../kategoriler";
|
import { kategoriEslesir } from "../kategoriler";
|
||||||
import {
|
import {
|
||||||
havuzOlustur,
|
havuzOlustur,
|
||||||
@@ -241,6 +245,18 @@ function secimiOnar(
|
|||||||
return { secim, onarim };
|
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;
|
const GEREKCE_UST_SINIR = 200;
|
||||||
|
|
||||||
// "Seçtiğin alanla eşleşiyor / alanına uygun": alan uyumu koddan bilinir
|
// "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
|
// --- 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.
|
||||||
|
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
|
// Adayın kendi söylediği sayılar ("300 km", "%50 burslu") metinde meşrudur
|
||||||
const profilSayilari = metindekiSayilar(...profilSatir.map((s) => s.metin));
|
const profilSayilari = metindekiSayilar(...profilSatir.map((s) => s.metin));
|
||||||
const sablonGerekceMetni = (p: AdayProgram): string => {
|
const sablonGerekceMetni = (p: AdayProgram): string => {
|
||||||
@@ -660,6 +678,8 @@ export async function raporUretAyrintili(
|
|||||||
if (m.length > GEREKCE_UST_SINIR) return sablon("uzun");
|
if (m.length > GEREKCE_UST_SINIR) return sablon("uzun");
|
||||||
const yasak = yasakIfadeler(m);
|
const yasak = yasakIfadeler(m);
|
||||||
if (yasak.length > 0) return sablon(`yasak ifade — ${yasak.join(" | ")}`);
|
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)) {
|
if (seciliKategoriler.length > 0 && !alanUyar(p) && ALAN_UYUMU_IDDIASI.test(m)) {
|
||||||
return sablon("alan uyumu iddiası, program seçili alanda değil");
|
return sablon("alan uyumu iddiası, program seçili alanda değil");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user