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:
@@ -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<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ığı ─────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user