fix(liste): liste metinlerinde deterministik çıktı denetimi — vaat dili kullanıcıya gitmez [KARAR]
Kök neden 1 (04-eval-sonuclari): yasak yalnız prompt'taydı; 37 listenin
14'ünün üst metninde, 888 gerekçenin 19'unda vaat dili / "garanti" vardı.
- liste-denetim.ts (yeni, saf fonksiyonlar): yasakIfadeler — Türkçe
çekimleri kapsayan dar kalıplar (garanti*, kesin, yüzde yüz, yerleşirsin,
rahat/kolay yerleşme, yerleşme şansı/ihtimali/olasılığı, şansın yüksek,
sırana rahat…). Reddeden kalıp ("garanti verilemez", "kesin değildir"),
"Güvenli" dilim etiketi, "%100 burslu", "kesin kayıt" ihlal DEĞİL.
dilimAdiniDuzelt — modelin dilim adı olarak yazdığı "garanti dilim/liste"
görünen ada ("güvenli") çevrilir. dayanaksizSayi — metindeki rakam girdide
yoksa ihlal (rakamlar koddan basılıyor).
- rapor.ts: her gerekçe, genel değerlendirme ve her uyarı denetimden geçer.
İhlalli gerekçe → koddan şablon gerekçe; ihlalli genel değerlendirme →
koddan (dağılım sayılarıyla) şablon; ihlalli uyarı atılır, 2'nin altına
düşerse koddan uyarı eklenir. Seçili alanın dışındaki programa "seçtiğin
alanla uyumlu" diyen gerekçe de şablona düşer. Ek model çağrısı YOK;
kredi ve gecikme etkilenmez. Canlı akışa Jev eklenmedi (eval'de hakem).
Önce → sonra (eval regex'i, 40 profil): vaat dili içeren üst metin
14/37 → 0/40, gerekçe 19/888 → 0/960; hakem (Jev) ihlali 30 → 1 satır.
Bedel: 960 gerekçenin 4'ü, 40 genel değerlendirmenin 2'si, 2 uyarı şablona düştü.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
146
src/lib/ai/liste-denetim.ts
Normal file
146
src/lib/ai/liste-denetim.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Liste metinlerinin (gerekçe, genel değerlendirme, uyarılar) DETERMİNİSTİK
|
||||
* çıktı denetimi. Model çağrısı yok, hakem yok: saf fonksiyonlar. Prompt
|
||||
* yasağı tek başına yetmiyor (ölçüm, 5 Eki 2026: 37 listenin 14'ünün üst
|
||||
* metninde, 888 gerekçenin 19'unda vaat dili); rapor.ts her metni buradan
|
||||
* geçirir, ihlalli metin kullanıcıya gitmez ve koddan üretilen şablona düşer.
|
||||
*
|
||||
* Bilinçli olarak DAR: yanlış alarmın bedeli bir metnin şablona düşmesi,
|
||||
* kaçırmanın bedeli kullanıcıya vaat gitmesi. Reddeden kalıplar ("garanti
|
||||
* verilemez", "kesin değildir") ihlal değildir; "Güvenli" dilim etiketi ihlal
|
||||
* değildir.
|
||||
*
|
||||
* server-only guard'ı yok: scripts/eval altından düz tsx ile de okunur.
|
||||
*/
|
||||
|
||||
const tr = (s: string) => s.toLocaleLowerCase("tr-TR");
|
||||
|
||||
// ───────────────────────── dilim adı: garanti → güvenli ─────────────────────────
|
||||
|
||||
// Üçüncü dilimin iç anahtarı `garanti`, kullanıcıya görünen adı "Güvenli"
|
||||
// (DILIM_ETIKET, lib/risk.ts). Model metinde dilimi iç anahtarıyla anarsa
|
||||
// ("garanti dilimde", "garanti listen") bu bir vaat değil yanlış etikettir:
|
||||
// atmak yerine görünen ada çevrilir. Yalnız dilim/liste adı konumunda.
|
||||
const DILIM_ADI_GARANTI =
|
||||
/(?<!\p{L})garant[iİ](?=["'“”‘’]?\s+(?:dilim|liste|tercih|seçenek|grub|kısım|kısm|satır)\p{L}*)/giu;
|
||||
|
||||
/** "garanti dilim/liste/tercih…" → "güvenli …" (büyük harf korunur). */
|
||||
export function dilimAdiniDuzelt(metin: string): string {
|
||||
return metin.replace(DILIM_ADI_GARANTI, (sozcuk) => (sozcuk[0] === "G" ? "Güvenli" : "güvenli"));
|
||||
}
|
||||
|
||||
// ───────────────────────── yasak ifade ─────────────────────────
|
||||
|
||||
// Eşleşmenin HEMEN ardından (aynı cümlecikte, araya noktalama girmeden) gelen
|
||||
// olumsuzlama: "garanti verilemez", "garantisi yoktur", "kesin değildir".
|
||||
const REDDEDEN =
|
||||
/^[^.,;:!?\n]{0,35}?(?<!\p{L})(?:değil|yok(?:tur)?(?!\p{L})|ver(?:il)?e?m[ei]\p{L}*|etme[zm]\p{L}*|etmiyor\p{L}*|edeme\p{L}*|edileme\p{L}*|sayılma\p{L}*|olmaz(?!sa)|sunma[zm]\p{L}*|sağlama[zm]\p{L}*|taşıma[zm]\p{L}*|anlamına gelme\p{L}*|hesaplam\p{L}*|hesaplanm\p{L}*|söylene?me\p{L}*|denile?me\p{L}*)/u;
|
||||
|
||||
// Adayın sihirbazda seçtiği öncelik "Kolay yerleşme": model bunu ad olarak
|
||||
// anabilir ("kolay yerleşme önceliğine uygun"); vaat cümlesi değildir.
|
||||
const ONCELIK_ADI = /^\s+(?:önceli|iste[ğk]|hedef|kriter|beklenti|tercihin)/u;
|
||||
|
||||
type Kalip = { ad: string; re: RegExp; istisna?: (ardi: string) => boolean };
|
||||
|
||||
const KALIPLAR: Kalip[] = [
|
||||
{ ad: "garanti", re: /garanti\p{L}*/gu },
|
||||
// "kesinleştir", "kesin kayıt" (ÖSYM terimi) hariç
|
||||
{ ad: "kesin", re: /(?<!\p{L})kesin(?:likle|dir|tir)?(?!\p{L})(?!\s+kayı[td])/gu },
|
||||
// "%100 burslu / %100 İngilizce" program adıdır
|
||||
{ ad: "yüzde yüz", re: /(?:yüzde\s+yüz|%\s?100)(?!\s*(?:burs|indirim|ingilizce|türkçe|almanca|fransızca|arapça))/gu },
|
||||
{
|
||||
ad: "yerleşirsin",
|
||||
re: /(?<!\p{L})(?:yerleş(?:ir|ecek|ebilir|iyor)s[iıuü]n(?:[iı]z|[uü]z)?|yerleş(?:ebil)?eceğin(?:iz)?|kazan(?:ır|acak|abilir)s[ıi]n(?:ız)?|(?:bölüme|programa|buraya|üniversiteye|okula|fakülteye)\s+gir(?:er|ecek|ebilir)s[iı]n(?:iz)?)/gu,
|
||||
},
|
||||
{
|
||||
ad: "rahat yerleşme",
|
||||
re: /(?<!\p{L})(?:rahat(?:ça|lıkla)?|kolayca|kolaylıkla)\s+(?:yerleş|gir(?!iş)|kazan)\p{L}*/gu,
|
||||
},
|
||||
// "sıralamanı rahatça karşılayan", "rahatlıkla tercih edilebilir": zarf hâli her bağlamda güvence verir
|
||||
{ ad: "rahatça", re: /(?<!\p{L})rahat(?:ça|lıkla)(?!\p{L})/gu },
|
||||
// "sırana rahat", "rahat bir tercih/seçenek", "sıralamana rahat oturur" (ölçüm: hakemin kalan
|
||||
// işaretlerinin çoğu). "rahat bir ortam/kampüs" yerleşmeyi değil yaşamı anlatır, ihlal değildir.
|
||||
{ ad: "rahat tercih", re: /(?<!\p{L})rahat\s+(?:bir\s+)?(?:tercih|seçenek|seçim|alternatif|otur|karşıla|yet)\p{L}*/gu },
|
||||
{ ad: "sırana rahat", re: /(?<!\p{L})sıra\p{L}*\s+(?:göre\s+)?rahat(?!\p{L})/gu },
|
||||
{ ad: "kolay yerleşme", re: /(?<!\p{L})kolay\s+yerleş\p{L}*/gu, istisna: (ardi) => ONCELIK_ADI.test(ardi) },
|
||||
{
|
||||
ad: "kesin yerleşme",
|
||||
re: /(?<!\p{L})(?:mutlaka|muhtemelen|büyük\s+ihtimalle|yüksek\s+ihtimalle|büyük\s+olasılıkla|yüksek\s+olasılıkla)\s+(?:yerleş|kazan)\p{L}*/gu,
|
||||
},
|
||||
// Ürün olasılık hesaplamaz: "yerleşme şansı/ihtimali/olasılığı" hiçbir yönde söylenmez
|
||||
{
|
||||
ad: "yerleşme olasılığı",
|
||||
re: /(?<!\p{L})(?:yerleşme|yerleşebilme|girme|kazanma|tutma|kabul)\s+(?:şans|ihtimal|olasılı)\p{L}*/gu,
|
||||
},
|
||||
{
|
||||
ad: "şans",
|
||||
re: /(?<!\p{L})şans\p{L}*\s+(?:çok\s+|oldukça\s+|daha\s+|epey\s+)?(?:yüksek|iyi|fazla|var|düşük|az|zayıf|artar|artır|yükselt)\p{L}*/gu,
|
||||
},
|
||||
{
|
||||
ad: "olasılık",
|
||||
re: /(?<!\p{L})(?:ihtimal|olasılı)\p{L}*\s+(?:çok\s+|oldukça\s+|daha\s+)?(?:yüksek|düşük|büyük|az|zayıf|güçlü|artar|artır)\p{L}*/gu,
|
||||
},
|
||||
{
|
||||
ad: "olasılık",
|
||||
re: /(?<!\p{L})(?:yüksek|büyük|güçlü|kuvvetli|düşük|zayıf)\s+(?:bir\s+)?(?:ihtimal|olasılı)\p{L}*/gu,
|
||||
},
|
||||
{ ad: "yüzde olasılık", re: /(?:%\s?\d+|yüzde\s+[\p{L}\d]+)\s*(?:ihtimal|olasılı|şans)\p{L}*/gu },
|
||||
];
|
||||
|
||||
/**
|
||||
* Metindeki vaat/olasılık/yasak sözcük ihlalleri (boş dizi = temiz). Çağıran
|
||||
* önce `dilimAdiniDuzelt` uygulamalıdır; burada kalan her "garanti" ihlaldir.
|
||||
*/
|
||||
export function yasakIfadeler(metin: string): string[] {
|
||||
const m = tr(metin);
|
||||
const bulunan: string[] = [];
|
||||
for (const k of KALIPLAR) {
|
||||
for (const e of m.matchAll(k.re)) {
|
||||
const son = (e.index ?? 0) + e[0].length;
|
||||
const ardi = m.slice(son, son + 60);
|
||||
if (REDDEDEN.test(ardi)) continue;
|
||||
if (k.istisna?.(ardi)) continue;
|
||||
bulunan.push(`${k.ad}: "${e[0]}"`);
|
||||
}
|
||||
}
|
||||
return bulunan;
|
||||
}
|
||||
|
||||
// ───────────────────────── sayı tutarlılığı ─────────────────────────
|
||||
|
||||
/**
|
||||
* Metindeki, `izinli` kümesinde karşılığı olmayan sayılar. Model rakam
|
||||
* yazmamalı (sıra, kontenjan, trend koddan basılıyor); yazdıysa girdiyle
|
||||
* birebir tutmalı. Atlananlar: 0–31 (adet, sıra no), 2015–2030 (yıl).
|
||||
* "45 bin" / "45B" / "45K" → izinli bir değerin bine yuvarlanmışı olmalı.
|
||||
*/
|
||||
export function dayanaksizSayi(metin: string, izinli: ReadonlySet<number>): string[] {
|
||||
const eksik: string[] = [];
|
||||
for (const e of metin.matchAll(/\d+(?:[.,]\d+)*/g)) {
|
||||
const t = e[0];
|
||||
const son = (e.index ?? 0) + t.length;
|
||||
const ardi = tr(metin.slice(son, son + 6));
|
||||
// "45.312" binlik; "21,7" / "3.5" ondalık → model ondalık üretmemeli
|
||||
const binlik = /^\d{1,3}(\.\d{3})+$/.test(t);
|
||||
if (!binlik && !/^\d+$/.test(t)) {
|
||||
eksik.push(t);
|
||||
continue;
|
||||
}
|
||||
const n = Number(t.replace(/\./g, ""));
|
||||
if (/^\s?(?:bin|b|k)(?!\p{L})/u.test(ardi)) {
|
||||
if (![...izinli].some((v) => Math.round(v / 1000) === n || Math.floor(v / 1000) === n)) eksik.push(`${t} bin`);
|
||||
continue;
|
||||
}
|
||||
if (n <= 31 || (n >= 2015 && n <= 2030) || izinli.has(n)) continue;
|
||||
eksik.push(t);
|
||||
}
|
||||
return eksik;
|
||||
}
|
||||
|
||||
/** Program adında/üniversite adında geçen sayılar ("%50 İndirimli", "7 Aralık") gerekçede meşrudur. */
|
||||
export function metindekiSayilar(...metinler: (string | null | undefined)[]): number[] {
|
||||
const cikti: number[] = [];
|
||||
// "50.000" binlik ayraçlı tek sayıdır
|
||||
for (const m of metinler) for (const e of (m ?? "").matchAll(/\d{1,3}(?:\.\d{3})+|\d+/g)) cikti.push(Number(e[0].replace(/\./g, "")));
|
||||
return cikti;
|
||||
}
|
||||
@@ -8,6 +8,12 @@ import type {
|
||||
} from "@/features/rapor/types/rapor";
|
||||
import { anahtarVar } from "./client";
|
||||
import { yapilandirilmisUret, type CagriGozlemi } from "./cagri";
|
||||
import {
|
||||
dayanaksizSayi,
|
||||
dilimAdiniDuzelt,
|
||||
metindekiSayilar,
|
||||
yasakIfadeler,
|
||||
} from "./liste-denetim";
|
||||
import { kategoriEslesir } from "../kategoriler";
|
||||
import {
|
||||
havuzOlustur,
|
||||
@@ -237,6 +243,13 @@ function secimiOnar(
|
||||
|
||||
const GEREKCE_UST_SINIR = 200;
|
||||
|
||||
// "Seçtiğin alanla eşleşiyor / alanına uygun": alan uyumu koddan bilinir
|
||||
// (kategoriEslesir). Alan gevşediğinde havuza seçili alanın DIŞINDAN program
|
||||
// girer; model bu satırlara da "seçtiğin alanla uyumlu" yazabiliyor (ölçüm:
|
||||
// a07, öğretmenlik seçen adaya Türk Dili ve Edebiyatı satırında).
|
||||
const ALAN_UYUMU_IDDIASI =
|
||||
/(?:seçtiğin(?:iz)?|ilgi)\s+alan\p{L}*\s+(?:\p{L}+\s+)?(?:uyumlu|uygun|eşleş\p{L}*|örtüş\p{L}*)|alan(?:ın|ınız)(?:la|a|ıza|ızla)\s+(?:birebir\s+|doğrudan\s+|tam\s+)?(?:uyumlu|uygun|eşleş\p{L}*|örtüş\p{L}*)/iu;
|
||||
|
||||
// İlk üretimde model zaman aşımına uğrarsa liste tamamen koddan kurulur
|
||||
// ("ne olursa olsun liste kurulur", kullanıcı kararı 7 Ağu 2026). Bu yolda
|
||||
// üretim BAŞARILI sayılır: rapor-actions krediyi iade etmez (paketsiz
|
||||
@@ -618,8 +631,10 @@ export async function raporUretAyrintili(
|
||||
}
|
||||
const nihai = enIyi;
|
||||
|
||||
// --- Şablon metinler: kodla tamamlanan satırın gerekçesi ve boş kalan üst
|
||||
// metin koddan basılır (ek model çağrısı yok).
|
||||
// --- Metin denetimi (deterministik; ek model çağrısı yok). İhlalli metin
|
||||
// kullanıcıya gitmez, koddan üretilen şablona düşer.
|
||||
// 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 => {
|
||||
const alan = alanUyar(p);
|
||||
const il = ilUyar(p);
|
||||
@@ -639,10 +654,30 @@ export async function raporUretAyrintili(
|
||||
return sablonGerekceMetni(p);
|
||||
};
|
||||
if (s.gerekce == null) return sablon("kodla tamamlandı");
|
||||
let m = s.gerekce.replace(/\s+/g, " ").trim();
|
||||
let m = dilimAdiniDuzelt(s.gerekce.replace(/\s+/g, " ").trim());
|
||||
if (m.length > GEREKCE_UST_SINIR) m = m.split(/(?<=[.!?])\s+/)[0];
|
||||
if (m.length < 8) return sablon("boş");
|
||||
if (m.length > GEREKCE_UST_SINIR) return sablon("uzun");
|
||||
const yasak = yasakIfadeler(m);
|
||||
if (yasak.length > 0) return sablon(`yasak ifade — ${yasak.join(" | ")}`);
|
||||
if (seciliKategoriler.length > 0 && !alanUyar(p) && ALAN_UYUMU_IDDIASI.test(m)) {
|
||||
return sablon("alan uyumu iddiası, program seçili alanda değil");
|
||||
}
|
||||
const sayi = dayanaksizSayi(
|
||||
m,
|
||||
new Set(
|
||||
[
|
||||
...p.siraGecmisi,
|
||||
p.efektifSira,
|
||||
p.kontenjanSon,
|
||||
p.yerlesenSon,
|
||||
params.sira,
|
||||
...metindekiSayilar(p.isim, p.universite),
|
||||
...profilSayilari,
|
||||
].filter((n): n is number => typeof n === "number"),
|
||||
),
|
||||
);
|
||||
if (sayi.length > 0) return sablon(`dayanaksız sayı — ${sayi.join(", ")}`);
|
||||
// Model bu programı başka dilimin listesine koymuştu; gerekçe o dilimi
|
||||
// anlatıyorsa (ör. güvenli diye yazıp hayale taşındı) metin yanıltır.
|
||||
if (s.kaynak === "tasindi" && /hayal|dengeli|güvenli|risk/i.test(m)) {
|
||||
@@ -651,6 +686,21 @@ export async function raporUretAyrintili(
|
||||
return m;
|
||||
};
|
||||
|
||||
const ustSayilar = new Set([
|
||||
params.sira,
|
||||
toplamHedef,
|
||||
hedef.hayal,
|
||||
hedef.dengeli,
|
||||
hedef.garanti,
|
||||
...profilSayilari,
|
||||
]);
|
||||
const ustMetinIhlali = (m: string): string | null => {
|
||||
const yasak = yasakIfadeler(m);
|
||||
if (yasak.length > 0) return `yasak ifade — ${yasak.join(" | ")}`;
|
||||
const sayi = dayanaksizSayi(m, ustSayilar);
|
||||
return sayi.length > 0 ? `dayanaksız sayı — ${sayi.join(", ")}` : null;
|
||||
};
|
||||
|
||||
const dagilimMetni = DILIMLER.filter((d) => hedef[d] > 0)
|
||||
.map((d) => `${hedef[d]} ${DILIM_ETIKET[d].toLocaleLowerCase("tr-TR")}`)
|
||||
.join(", ");
|
||||
@@ -659,21 +709,26 @@ export async function raporUretAyrintili(
|
||||
? `Listen ${toplamHedef} tercihten oluşuyor: ${dagilimMetni}. Yapay zekâ bu sefer zamanında yanıt vermediği için liste, sıralamana ve seçimlerine göre aday havuzundan otomatik kuruldu; satır açıklamaları standart metindir.`
|
||||
: `Listen ${toplamHedef} tercihten oluşuyor: ${dagilimMetni}. Dilimler, programların son taban sıralaması senin sıranla kıyaslanarak belirlendi.`;
|
||||
let genelSablon: string | null = null;
|
||||
let genel = nihai.genel.replace(/\s+/g, " ").trim();
|
||||
let genel = dilimAdiniDuzelt(nihai.genel.replace(/\s+/g, " ").trim());
|
||||
if (kaynak === "deterministik") {
|
||||
genelSablon = "model zaman aşımı";
|
||||
} else if (!genel) {
|
||||
genelSablon = "boş";
|
||||
} else {
|
||||
genelSablon = ustMetinIhlali(genel);
|
||||
}
|
||||
if (genelSablon) genel = sablonGenel;
|
||||
|
||||
const uyariAtilan: string[] = [];
|
||||
const uyarilar: string[] = [];
|
||||
for (const ham of nihai.uyarilar) {
|
||||
const m = ham.replace(/\s+/g, " ").trim();
|
||||
if (m) uyarilar.push(m);
|
||||
const m = dilimAdiniDuzelt(ham.replace(/\s+/g, " ").trim());
|
||||
if (!m) continue;
|
||||
const ihlal = ustMetinIhlali(m);
|
||||
if (ihlal) uyariAtilan.push(ihlal);
|
||||
else uyarilar.push(m);
|
||||
}
|
||||
// Model en az 2 uyarı yazar; azı geldiyse (ya da liste koddan kurulduysa) eksik koddan tamamlanır.
|
||||
// Model en az 2 uyarı yazar; denetimden 2'den azı geçtiyse eksik koddan tamamlanır.
|
||||
const sablonUyarilar: string[] = [
|
||||
...(gevsetildi.includes("profil")
|
||||
? ["Danışmana söylediğin tercihlerle yeterli program bulunamadığı için liste bu tercihler uygulanmadan kuruldu."]
|
||||
|
||||
Reference in New Issue
Block a user