fix(danisman): L1-L3, L5, L6 (9) — Türkçe-duyarsız il/bölüm/üniversite eşleşmesi, kısa girdi hatası

- ilAdiNormalize harf katlayarak eşler: IZMIR / izmir / İzmir → İZMİR (program_ara,
  il_ozeti, profil evIl). il_ozeti "Afyon"u da bulur.
- program_ara bölüm ve üniversite aramasını katlanmış adla yapar ("isletme",
  "bogazici", "ODTÜ"); LIKE jokerleri (%/_) artık her şeyi eşlemez.
- Tek harf / boş girdi ilk kaydı (Abaza Dili, Abdullah Gül) döndürmez; "en az 3
  harf" hatası döner.
- Kısaltma tablosunda bozuk satır: dtu → deu (Dokuz Eylül).
- Akış hata logu tam hata nesnesi yerine mesajı basar; cagri.ts kullanılmayan import.
- (8'in devamı) dil seçenekleri veride olan dillerle eşitlendi; doğrulama
  "Almanca" süzgecini reddetmesin.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-05 01:10:16 +03:00
parent 72b673bb94
commit b26df27fcd
4 changed files with 65 additions and 41 deletions

View File

@@ -90,7 +90,7 @@ export const ARACLAR: AracTanimi[] = [
universite: { type: "string", description: "Üniversite adı parçası" },
il: { type: "string", description: "İl adı, ör. 'İzmir'" },
tur: { type: "string", enum: ["devlet", "vakif"], description: "Yalnız devlet ya da yalnız vakıf üniversiteleri" },
dil: { type: "string", enum: ["Türkçe", "İngilizce"], description: "Öğretim dili" },
dil: { type: "string", enum: ["Türkçe", "İngilizce", "Almanca", "Fransızca", "Arapça", "Rusça"], description: "Öğretim dili" },
ogretim_turu: {
type: "string",
enum: ["Örgün Öğretim", "Uzaktan Öğretim", "Açık Öğretim"],
@@ -318,23 +318,38 @@ function programOzeti(p: Program, sira: number, o?: ProgramOzellik) {
};
}
/** SQLite LIKE Türkçe büyük/küçük harfi bilmez: hem yazıldığı gibi hem başlık düzeni denenir. */
function likeVaryantlari(q: string): string[] {
const t = q.trim();
const baslik = t
.toLocaleLowerCase("tr-TR")
.split(" ")
.map((w) => (w ? w[0].toLocaleUpperCase("tr-TR") + w.slice(1) : w))
.join(" ");
return Array.from(new Set([t, baslik, t.toLocaleUpperCase("tr-TR")]));
/** Arama metninin katlanmış hâli en az bu kadar harf olmalı; daha kısası her kayıtla eşleşir. */
const ARAMA_ALT_HARF = 3;
/** Yaygın üniversite kısaltmaları → adın katlanmış (slug) parçası. */
const UNI_KISALTMA: Record<string, string> = {
odtu: "orta-dogu-teknik",
itu: "istanbul-teknik",
ytu: "yildiz-teknik",
ktu: "karadeniz-teknik",
deu: "dokuz-eylul",
};
const ayrikDegerler = new Map<string, { ham: string; katli: string }[]>();
/**
* Kolonun, katlanmış hâli `katliSorgu`yu içeren değerleri. SQLite LIKE Türkçe
* harf katlamayı bilmez ("isletme" İşletme'yi, "bogazici" Boğaziçi'ni bulmaz);
* ayrık değerler (birkaç bin) bir kez okunur, eşleşme JS'te yapılır.
*/
function katliEslesenler(kolon: "isim" | "universite", katliSorgu: string): string[] {
let liste = ayrikDegerler.get(kolon);
if (!liste) {
liste = (getDb().prepare(`SELECT DISTINCT ${kolon} AS v FROM programs`).all() as { v: string | null }[])
.filter((x): x is { v: string } => x.v != null)
.map((x) => ({ ham: x.v, katli: turkishSlugify(x.v) }));
ayrikDegerler.set(kolon, liste);
}
return liste.filter((x) => x.katli.includes(katliSorgu)).map((x) => x.ham);
}
function likeKosulu(kolon: string, q: string): { sql: string; args: string[] } {
const v = likeVaryantlari(q);
return {
sql: `(${v.map(() => `${kolon} LIKE ?`).join(" OR ")})`,
args: v.map((x) => `%${x}%`),
};
function inKosulu(kolon: string, degerler: string[]): string {
return degerler.length ? `${kolon} IN (${degerler.map(() => "?").join(",")})` : "0 = 1";
}
// ── Araç gövdeleri ───────────────────────────────────────────────────────
@@ -363,24 +378,28 @@ function programAra(b: AracBaglami, a: ProgramAraArg, profilUygula = true): Prog
const args: (string | number)[] = [PUAN_TURLERI[b.tur], b.tur === "tyt" ? 1 : 0];
const uygulanan: string[] = [];
const bos = (not: string): ProgramAraSonuc => ({ toplamEslesen: 0, gosterilen: 0, uygulananFiltreler: [], sonuclar: [], not });
if (a.arama) {
const k = likeKosulu("isim", a.arama);
kosullar.push(k.sql);
args.push(...k.args);
const katli = turkishSlugify(a.arama);
if (katli.length < ARAMA_ALT_HARF) return bos(`Bölüm araması için en az ${ARAMA_ALT_HARF} harf yaz: ${a.arama}`);
const adlar = katliEslesenler("isim", katli);
kosullar.push(inKosulu("isim", adlar));
args.push(...adlar);
uygulanan.push(`bölüm ~ "${a.arama}"`);
}
if (a.universite) {
const k = likeKosulu("universite", a.universite);
kosullar.push(k.sql);
args.push(...k.args);
const slug = turkishSlugify(a.universite);
const katli = UNI_KISALTMA[slug] ?? slug;
if (katli.length < ARAMA_ALT_HARF) return bos(`Üniversite araması için en az ${ARAMA_ALT_HARF} harf yaz: ${a.universite}`);
const adlar = katliEslesenler("universite", katli);
kosullar.push(inKosulu("universite", adlar));
args.push(...adlar);
uygulanan.push(`üniversite ~ "${a.universite}"`);
}
// İl: parametre > profildeki km sınırı
if (a.il) {
const il = ilAdiNormalize(a.il);
if (!il) {
return { toplamEslesen: 0, gosterilen: 0, uygulananFiltreler: [], sonuclar: [], not: `İl tanınmadı: ${a.il}` };
}
if (!il) return bos(`İl tanınmadı: ${a.il}`);
kosullar.push("il = ?");
args.push(il);
uygulanan.push(`il = ${il}`);
@@ -594,6 +613,7 @@ function bolumIstihdam(b: AracBaglami, a: { bolumler?: unknown }) {
ulkeOrtalamasi: ISTIHDAM_ORTALAMA[duzey],
bolumler: adlar.map((ad) => {
const slug = bolumSayfaSlug(ad);
if (slug.length < ARAMA_ALT_HARF) return { aranan: ad, hata: `Bölüm adı çok kısa; en az ${ARAMA_ALT_HARF} harf yaz.` };
// Birebir > başı tutan > içinde geçen ("bilgisayar müh" → bilgisayar-muhendisligi)
const anahtar =
(slug in tablo ? slug : undefined) ??
@@ -619,6 +639,8 @@ function bolumOzeti(_b: AracBaglami, a: { bolum?: string }) {
const q = String(a.bolum ?? "").trim();
if (!q) return { hata: "Bölüm adı gerekli." };
const slug = bolumSayfaSlug(q);
// Boş/tek harfli slug her bölümle "başlıyor" sayılır ve ilk kayıt (Abaza Dili) dönerdi
if (slug.length < ARAMA_ALT_HARF) return { hata: `Bölüm adı çok kısa; en az ${ARAMA_ALT_HARF} harf yaz: ${q}` };
const hepsi = getAllBolumler();
const aday =
hepsi.find((x) => x.slug === slug) ??
@@ -645,7 +667,8 @@ const IL_UNI_UST = 30;
function ilOzeti(_b: AracBaglami, a: { il?: string }) {
const q = String(a.il ?? "").trim();
if (!q) return { hata: "İl adı gerekli." };
const il = getIlBySlug(turkishSlugify(q));
// "Afyon" → AFYONKARAHİSAR: program_ara ile aynı il eşleşmesi
const il = getIlBySlug(turkishSlugify(ilAdiNormalize(q) ?? q));
if (!il) return { hata: `İl bulunamadı: ${q}` };
return {
il: il.ad,
@@ -724,14 +747,8 @@ function universiteProfili(_b: AracBaglami, a: { universite?: string }) {
if (!q) return { hata: "Üniversite adı gerekli." };
const slugQ = turkishSlugify(q);
const hepsi = getAllUniversiteler();
const kisaltma: Record<string, string> = {
odtu: "orta-dogu-teknik",
itu: "istanbul-teknik",
ytu: "yildiz-teknik",
ktu: "karadeniz-teknik",
dtu: "deu",
};
const arananSlug = kisaltma[slugQ] ?? slugQ;
const arananSlug = UNI_KISALTMA[slugQ] ?? slugQ;
if (arananSlug.length < ARAMA_ALT_HARF) return { hata: `Üniversite adı çok kısa; en az ${ARAMA_ALT_HARF} harf yaz: ${q}` };
const aday =
hepsi.find((u) => u.slug === arananSlug || u.slug === `${arananSlug}-universitesi`) ??
hepsi.find((u) => u.slug.startsWith(arananSlug)) ??

View File

@@ -1,4 +1,3 @@
import type OpenAI from "openai";
import { z } from "zod";
import { getOpenRouter, raporModeli } from "./client";

View File

@@ -117,7 +117,8 @@ export function danismanAkisi(opts: {
writer.write({ type: "finish" });
} catch (err) {
if (!tamamlandi) await opts.onYarim(metin).catch(() => {});
console.error("[soru] stream hatası:", err);
// Tam hata nesnesi istek gövdesini ve cevap başlıklarını taşır; yalnız mesaj
console.error("[soru] stream hatası:", err instanceof Error ? err.message : String(err));
// Model hatası akışa zaten "error" parçası olarak yazıldı; yazılmadıysa
// (boş cevap, kayıt hatası) istemci hatayı buradan görür.
if (akisHatasi == null) throw err;

View File

@@ -1,5 +1,6 @@
// İl merkezleri arası kuş uçuşu mesafe (km). Profildeki "evden en fazla N km"
// filtresi il düzeyinde çalışır: adres/koordinat tutulmaz (KVKK, §8).
import { turkishSlugify } from "./slug";
import { IL_MERKEZ, type LatLng } from "./uni-koordinatlar";
const DUNYA_YARICAP_KM = 6371;
@@ -14,13 +15,19 @@ export function kmMesafe(a: LatLng, b: LatLng): number {
return Math.round(2 * DUNYA_YARICAP_KM * Math.asin(Math.sqrt(h)));
}
/** DB il adı biçimine (büyük harf, Türkçe) çevirir; bilinmiyorsa null. */
/**
* DB il adı biçimine (büyük harf, Türkçe) çevirir; bilinmiyorsa null.
* Türkçe harfler katlanarak karşılaştırılır: "IZMIR", "izmir", "İzmir" → İZMİR.
*/
export function ilAdiNormalize(ham: string): string | null {
const aday = ham.trim().toLocaleUpperCase("tr-TR");
if (aday in IL_MERKEZ) return aday;
const aday = turkishSlugify(ham);
if (!aday) return null;
const iller = Object.keys(IL_MERKEZ).map((il) => ({ il, katli: turkishSlugify(il) }));
const tam = iller.find((x) => x.katli === aday);
if (tam) return tam.il;
// "Afyon" → AFYONKARAHİSAR gibi öneki eşleşen tek il varsa onu al
const adaylar = Object.keys(IL_MERKEZ).filter((il) => il.startsWith(aday));
return adaylar.length === 1 ? adaylar[0] : null;
const adaylar = iller.filter((x) => x.katli.startsWith(aday));
return adaylar.length === 1 ? adaylar[0].il : null;
}
/** Ev iline en fazla `km` uzaktaki iller (evin kendisi dahil), yakından uzağa. */