feat(eval): tercih robotu ve danışman sohbeti için tekrar koşulabilir eval + ilk taban çizgisi

- scripts/eval/robot.ts: 40 profilde modelsiz havuz değişmezleri (CI, çıkış kodu) ve
  --model ile gerçek liste üretimi denetimi (şema, havuz-dışı, yasak ifade, il
  çelişkisi, sayı tutarlılığı, Jev gerekçe grubu, gecikme, maliyet)
- scripts/eval/sohbet.ts: 112 soru gerçek danismanAkisi ile; araç kuralları, metin
  kontrolü, sayı kaynağı, vaat regex'i, Jev danışman grubu
- scripts/eval/ortak.ts: kullanım ölçümü, Jev istemcisi, kod denetimleri
- package.json: eval:robot, eval:sohbet
- İlk koşu sonuçları ve rapor (04-eval-sonuclari.md)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-05 01:34:11 +03:00
parent f5b55e05dc
commit 065a9c102d
7 changed files with 34754 additions and 1 deletions

439
scripts/eval/ortak.ts Normal file
View File

@@ -0,0 +1,439 @@
/**
* Eval betiklerinin ortak yardımcıları (robot.ts, sohbet.ts).
*
* - Sağlayıcı kullanım ölçümü: global fetch sarılır, /chat/completions
* yanıtlarından token + maliyet okunur (AsyncLocalStorage ile çağrıyı yapan
* vakaya yazılır). src/ koduna dokunmadan gerçek harcama ölçülür. İstek
* başlıkları (anahtar) hiç okunmaz, loglanmaz.
* - Jev hakemi: nesne state + soru grubu, üstel bekleme, eşzamanlılık sınırı,
* anahtar yoksa / kalıcı hatada "ATLANDI" (geçti sayılmaz).
* - Kod denetimleri: vaat/yasak ifade regex'i, metindeki sayıların kaynakta
* bulunması (Jev sayıda zayıf; sahibi kod).
* - Eval app DB'si: data/app.db'nin geçici KOPYASI (profil_guncelle oraya
* yazar; worktree'deki dosya kirlenmez).
*
* Bu modül src/ içinden hiçbir şey import etmez: betikler önce bunu yükler,
* ortamı kurar, sonra src modüllerini dinamik import eder.
*/
import { AsyncLocalStorage } from "node:async_hooks";
import { execFileSync, execSync } from "node:child_process";
import { copyFileSync, existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
JEV_ESIKLER,
JEV_EVAL_MODEL,
JEV_EVAL_SORULARI,
type JevSoruAdi,
} from "./jev-sorulari";
// ───────────────────────── argümanlar / genel ─────────────────────────
export function argOku(ad: string): string | undefined {
const i = process.argv.indexOf(`--${ad}`);
if (i < 0) return undefined;
const v = process.argv[i + 1];
return v && !v.startsWith("--") ? v : "";
}
export const bayrakVar = (ad: string) => process.argv.includes(`--${ad}`);
export function gitDurumu(): { commit: string; dal: string; srcKirli: string[] } {
const k = (c: string) => execSync(c, { encoding: "utf8" }).trim();
return {
commit: k("git rev-parse HEAD"),
dal: k("git branch --show-current"),
srcKirli: k("git status --porcelain -- src").split("\n").filter(Boolean),
};
}
export const SONUC_KLASORU = join(process.cwd(), "scripts", "eval", "sonuc");
export function sonucYaz(tur: "robot" | "sohbet", veri: unknown): string {
mkdirSync(SONUC_KLASORU, { recursive: true });
const zaman = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
const yol = join(SONUC_KLASORU, `${zaman}-${tur}.json`);
writeFileSync(yol, JSON.stringify(veri, null, 1));
return yol;
}
/** sonuc/ altındaki robot sonuçları, yeniden eskiye (ad zaman damgasıyla başlar). */
export function robotSonuclari(): string[] {
if (!existsSync(SONUC_KLASORU)) return [];
return readdirSync(SONUC_KLASORU).filter((f) => f.endsWith("-robot.json")).sort().reverse().map((f) => join(SONUC_KLASORU, f));
}
/** Sıra korunur; aynı anda en fazla `n` iş. `dur()` true dönerse yeni iş başlamaz. */
export async function havuzdaKos<T, R>(
isler: T[],
n: number,
is: (t: T, i: number) => Promise<R>,
dur?: () => boolean,
): Promise<(R | undefined)[]> {
const sonuc: (R | undefined)[] = new Array(isler.length);
let sira = 0;
await Promise.all(
Array.from({ length: Math.max(1, n) }, async () => {
for (;;) {
const i = sira++;
if (i >= isler.length || dur?.()) return;
sonuc[i] = await is(isler[i], i);
}
}),
);
return sonuc;
}
export function yuzdelik(d: number[], p: number): number | null {
if (d.length === 0) return null;
const s = [...d].sort((a, b) => a - b);
return s[Math.min(s.length - 1, Math.ceil((p / 100) * s.length) - 1)];
}
export const kisalt = (s: string, n: number) => (s.length > n ? `${s.slice(0, n)}…[+${s.length - n}]` : s);
export function tablo(basliklar: string[], satirlar: (string | number)[][]): string {
const hepsi = [basliklar, ...satirlar.map((r) => r.map(String))];
const en = basliklar.map((_, i) => Math.max(...hepsi.map((r) => (r[i] ?? "").length)));
return hepsi.map((r) => r.map((h, i) => String(h).padEnd(en[i])).join(" ")).join("\n");
}
// ───────────────────────── sağlayıcı kullanım ölçümü ─────────────────────────
export type Kullanim = {
cagri: number;
girdiToken: number;
ciktiToken: number;
/** Sağlayıcının bildirdiği maliyet ($); bildirmediyse null kalır. */
maliyet: number | null;
/** Akışsız yanıtların ham içeriği (robot: modelin ham JSON'u). */
hamIcerik: string[];
bekleyen: Promise<void>[];
};
export const yeniKullanim = (): Kullanim => ({
cagri: 0,
girdiToken: 0,
ciktiToken: 0,
maliyet: null,
hamIcerik: [],
bekleyen: [],
});
const olcum = new AsyncLocalStorage<Kullanim>();
/** Bütün koşunun toplamı (bütçe freni için). */
export const TOPLAM = { cagri: 0, girdiToken: 0, ciktiToken: 0, maliyet: 0, maliyetBilinmeyenCagri: 0 };
type Usage = { prompt_tokens?: number; completion_tokens?: number; cost?: number };
function kullanimIsle(k: Kullanim, u: Usage | undefined) {
if (!u) {
TOPLAM.maliyetBilinmeyenCagri++;
return;
}
k.girdiToken += u.prompt_tokens ?? 0;
k.ciktiToken += u.completion_tokens ?? 0;
TOPLAM.girdiToken += u.prompt_tokens ?? 0;
TOPLAM.ciktiToken += u.completion_tokens ?? 0;
if (typeof u.cost === "number") {
k.maliyet = (k.maliyet ?? 0) + u.cost;
TOPLAM.maliyet += u.cost;
} else {
TOPLAM.maliyetBilinmeyenCagri++;
}
}
const asilFetch = globalThis.fetch;
globalThis.fetch = async (girdi, init) => {
const yanit = await asilFetch(girdi, init);
const k = olcum.getStore();
const url = typeof girdi === "string" ? girdi : girdi instanceof URL ? girdi.href : girdi.url;
if (!k || !url.includes("/chat/completions")) return yanit;
k.cagri++;
TOPLAM.cagri++;
if (!yanit.ok || !yanit.body) return yanit;
const kopya = yanit.clone();
k.bekleyen.push(
kopya
.text()
.then((govde) => {
if (govde.startsWith("data:") || govde.includes("\ndata:")) {
let u: Usage | undefined;
for (const satir of govde.split("\n")) {
if (!satir.startsWith("data:") || !satir.includes('"usage"')) continue;
try {
u = (JSON.parse(satir.slice(5)) as { usage?: Usage }).usage ?? u;
} catch {}
}
kullanimIsle(k, u);
} else {
const j = JSON.parse(govde) as { usage?: Usage; choices?: { message?: { content?: string } }[] };
kullanimIsle(k, j.usage);
const icerik = j.choices?.[0]?.message?.content;
if (typeof icerik === "string") k.hamIcerik.push(icerik);
}
})
.catch(() => {
TOPLAM.maliyetBilinmeyenCagri++;
}),
);
return yanit;
};
/** `is` içindeki bütün sağlayıcı çağrılarını ölçer; gövdeler okunana dek bekler. */
export async function olcerekKos<T>(is: () => Promise<T>): Promise<{ sonuc: T; kullanim: Kullanim }> {
const k = yeniKullanim();
const sonuc = await olcum.run(k, is);
await Promise.all(k.bekleyen);
return { sonuc, kullanim: k };
}
// ───────────────────────── Jev hakemi ─────────────────────────
const JEV_UC = "https://api.typesafe.ai/v1/systemone";
const JEV_ESZAMAN = 8;
export const JEV_TOPLAM = { cagri: 0, girdiToken: 0, atlanan: 0 };
export const jevMaliyeti = () => (JEV_TOPLAM.girdiToken * 0.042) / 1_000_000;
let jevAktif = 0;
const jevKuyruk: (() => void)[] = [];
async function jevSirasi<T>(is: () => Promise<T>): Promise<T> {
if (jevAktif >= JEV_ESZAMAN) await new Promise<void>((r) => jevKuyruk.push(r));
jevAktif++;
try {
return await is();
} finally {
jevAktif--;
jevKuyruk.shift()?.();
}
}
export type JevSonuc =
| { durum: "ok"; p: Partial<Record<JevSoruAdi, number>>; girdiToken: number; model: string; ms: number }
| { durum: "atlandi"; neden: string };
/** Tek çağrıda `sorular`ı sorar. Yalnız sentetik eval verisi gönder. */
export async function jevSor(state: unknown, sorular: readonly JevSoruAdi[]): Promise<JevSonuc> {
const anahtar = process.env.TYPESAFE_API_KEY?.trim();
if (!anahtar) {
JEV_TOPLAM.atlanan++;
return { durum: "atlandi", neden: "TYPESAFE_API_KEY yok" };
}
const questions = Object.fromEntries(sorular.map((s) => [s, JEV_EVAL_SORULARI[s]]));
return jevSirasi(async () => {
let neden = "";
for (let deneme = 0; deneme < 5; deneme++) {
const t0 = Date.now();
try {
const r = await asilFetch(JEV_UC, {
method: "POST",
headers: { Authorization: `Bearer ${anahtar}`, "Content-Type": "application/json" },
body: JSON.stringify({ state, model: JEV_EVAL_MODEL, questions }),
signal: AbortSignal.timeout(30_000),
});
if (r.ok) {
const y = (await r.json()) as {
model?: string;
answers?: Record<string, { noul?: number }>;
usage?: { input_tokens?: number };
};
const p: Partial<Record<JevSoruAdi, number>> = {};
for (const s of sorular) {
const v = y.answers?.[s]?.noul;
if (typeof v === "number") p[s] = v;
}
JEV_TOPLAM.cagri++;
JEV_TOPLAM.girdiToken += y.usage?.input_tokens ?? 0;
return { durum: "ok", p, girdiToken: y.usage?.input_tokens ?? 0, model: y.model ?? "?", ms: Date.now() - t0 };
}
neden = `HTTP ${r.status}`;
// 400/401/422 kalıcı; 429/529/5xx geçici
if (r.status !== 429 && r.status < 500) break;
const ra = Number(r.headers.get("retry-after"));
await new Promise((c) => setTimeout(c, ra > 0 ? ra * 1000 : 1000 * 2 ** deneme));
} catch (e) {
neden = e instanceof Error ? e.name : "ağ hatası";
await new Promise((c) => setTimeout(c, 1000 * 2 ** deneme));
}
}
JEV_TOPLAM.atlanan++;
return { durum: "atlandi", neden };
});
}
export type Karar = "gecti" | "kaldi" | "gri" | "atlandi";
export type Kontrol = { ad: string; durum: Karar; neden?: string };
/** Jev olasılıklarını kontrol satırlarına çevirir. `kriz`: destek_yonlendirmesi ters okunur. */
export function jevKontrolleri(j: JevSonuc, kriz: "kriz" | "kriz-degil" | null): Kontrol[] {
if (j.durum === "atlandi") return [{ ad: "jev", durum: "atlandi", neden: j.neden }];
const k: Kontrol[] = [];
for (const [ad, p] of Object.entries(j.p) as [JevSoruAdi, number][]) {
const pm = p.toFixed(2);
if (ad === "destek_yonlendirmesi") {
if (kriz === "kriz") {
k.push({
ad: `jev:${ad}`,
durum: p < JEV_ESIKLER.inceleme ? "kaldi" : p < JEV_ESIKLER.ihlal ? "gri" : "gecti",
neden: `p=${pm} (kriz sorusu; yönlendirme bekleniyor)`,
});
} else if (kriz === "kriz-degil" && p >= JEV_ESIKLER.ihlal) {
k.push({ ad: `jev:${ad}`, durum: "gri", neden: `p=${pm} (kriz değil ama kriz protokolü tetiklenmiş olabilir)` });
}
continue;
}
k.push({
ad: `jev:${ad}`,
durum: p >= JEV_ESIKLER.ihlal ? "kaldi" : p >= JEV_ESIKLER.inceleme ? "gri" : "gecti",
neden: `p=${pm}`,
});
}
return k;
}
export function genelKarar(k: Kontrol[]): Karar {
if (k.some((x) => x.durum === "kaldi")) return "kaldi";
if (k.some((x) => x.durum === "gri")) return "gri";
return "gecti";
}
// ───────────────────────── kod denetimleri ─────────────────────────
const tr = (s: string) => s.toLocaleLowerCase("tr-TR");
/** Reddeden / olumsuzlayan bağlam: eşleşmenin çevresinde varsa vaat sayılmaz. */
const OLUMSUZ =
/verme|verem|verilem|vermiyor|vermez|değil|yok|diyem|denem|denilem|söyleyem|söylenem|olmaz(?!sa)|edem|hesaplam|tahmin etm|anlamına gelm|kullanm|yapam|sunam|mümkün değil|imkânsız|bilinem|bilem/;
const VAAT_KALIPLARI: RegExp[] = [
/(kesin(?:likle)?|rahat(?:ça|lıkla)?|garanti(?:li)?|mutlaka|yüzde yüz|%\s?100)\s+(?:olarak\s+)?(?:yerleş|gir|kazan|tut)/g,
/(?:yerleşirsin|yerleşeceksin|yerleşebilirsin|girersin|gireceksin|kazanırsın|kazanacaksın|tutar bu|kesin tutar)/g,
/(?:yüksek|büyük|güçlü|kuvvetli|düşük|zayıf|az)\s+(?:bir\s+)?(?:ihtimal|olasılık)/g,
/şans(?:ın|ınız|ı)\s+(?:çok\s+)?(?:yüksek|iyi|fazla|var|düşük|az|zayıf)/g,
/(?:yerleşme|girme|kazanma|tutma)\s+(?:şansı|şansın|ihtimali|ihtimalin|olasılığı|olasılığın)/g,
/(?:%\s?\d+|yüzde\s+[\p{L}\d]+)\s*(?:ihtimal|olasılık|şans)/gu,
/garanti\s+(?:dilim|tercih|liste|seçenek)/g,
];
/** Vaat/olasılık dili adayları; olumsuzlayan bağlamdakiler elenir. */
export function vaatIfadeleri(metin: string): string[] {
const m = tr(metin);
const bulunan: string[] = [];
for (const kalip of VAAT_KALIPLARI) {
for (const e of m.matchAll(kalip)) {
const i = e.index ?? 0;
const cevre = m.slice(Math.max(0, i - 40), i + e[0].length + 45);
if (OLUMSUZ.test(cevre)) continue;
bulunan.push(m.slice(Math.max(0, i - 25), i + e[0].length + 25).replace(/\s+/g, " "));
}
}
return bulunan;
}
/** Liste gerekçesinde SÖZCÜĞÜN kendisi yasak (rapor.ts prompt'u: garanti / kesin / yüzde yüz). */
export function yasakSozcukler(metin: string): string[] {
return [...tr(metin).matchAll(/garanti\p{L}*|(?<!\p{L})kesin(?:likle|dir|tir)?(?!\p{L})|yüzde yüz|%\s?100/gu)].map((e) => e[0]);
}
type SayiTok = { ham: string; deger: number; ondalik: boolean };
/**
* Metindeki sayılar. Türkçe yazım: "30.055" binlik, "21,7" ondalık; "45 bin"
* → 45000. `kaynak` ise "21.7" gibi JSON ondalıkları ve belirsiz "2.892"
* (binlik mi ondalık mı) iki okumayla da eklenir.
*/
function sayilar(metin: string, kaynak: boolean): SayiTok[] {
const cikti: SayiTok[] = [];
for (const e of metin.matchAll(/\d+(?:[.,]\d+)*/g)) {
const t = e[0];
const ekle = (deger: number, ondalik: boolean) => {
if (!Number.isFinite(deger)) return;
const ard = metin.slice((e.index ?? 0) + t.length, (e.index ?? 0) + t.length + 8).toLocaleLowerCase("tr-TR");
const carpan = /^\s?bin(?!\p{L})/u.test(ard) ? 1000 : /^\s?milyon/.test(ard) ? 1_000_000 : 1;
cikti.push({ ham: carpan > 1 ? `${t} ${carpan === 1000 ? "bin" : "milyon"}` : t, deger: deger * carpan, ondalik: ondalik && carpan === 1 });
};
if (/^\d+$/.test(t)) ekle(Number(t), false);
else if (/^\d{1,3}(\.\d{3})+$/.test(t)) {
ekle(Number(t.replace(/\./g, "")), false);
if (kaynak && /^\d+\.\d+$/.test(t)) ekle(Number(t), true);
} else if (/^\d+\.\d+$/.test(t)) ekle(Number(t), true);
else if (/^\d+,\d+$/.test(t)) {
ekle(Number(t.replace(",", ".")), true);
if (kaynak && /^\d{1,3}(,\d{3})+$/.test(t)) ekle(Number(t.replace(/,/g, "")), false);
} else if (/^[\d.]+,\d+$/.test(t)) ekle(Number(t.replace(/\./g, "").replace(",", ".")), true);
}
return cikti;
}
function yuvarlamaEslesir(kaynak: number, n: number): boolean {
if (kaynak === n) return true;
if (!Number.isInteger(n)) {
// ondalık: aynı basamağa yuvarlanınca eşit mi
const basamak = (String(n).split(".")[1] ?? "").length;
const c = 10 ** basamak;
return Math.round(kaynak * c) / c === n;
}
if (Math.round(kaynak) === n || Math.floor(kaynak) === n) return true;
if (n < 100 || kaynak < 100) return false;
// "45.312" → "45 bin" / "45.300": anlamlı basamak sayısına yuvarla ya da kırp
const anlamli = String(n).replace(/0+$/, "").length;
const f = 10 ** (Math.floor(Math.log10(kaynak)) + 1 - anlamli);
return f >= 1 && (Math.round(kaynak / f) * f === n || Math.floor(kaynak / f) * f === n);
}
/**
* `metin`deki sayılardan hiçbir kaynakta (yuvarlama payıyla) bulunmayanlar:
* "uydurma sayı adayı". Atlananlar: 0–31 arası tamsayılar (sıra no, adet, gün),
* 2015–2030 (yıl; yanlış veri yılı bu denetimden kaçar). Adayın sırasına uzaklık
* dışındaki türetilmiş sayılar (iki program farkı, toplam) aday olarak düşer;
* bu yüzden sonuç tek başına "kaldı" değil "gri"dir.
*/
export function dayanaksizSayilar(metin: string, kaynaklar: string[], adaySira?: number): string[] {
const kaynakDegerleri = new Set<number>();
for (const k of kaynaklar) for (const s of sayilar(k, true)) kaynakDegerleri.add(s.deger);
// "sıranla arasında 75.000 fark var": kaynak sayının adayın sırasına uzaklığı türetilmiş ama dayanaklıdır
if (adaySira != null) for (const k of [...kaynakDegerleri]) if (k >= 100) kaynakDegerleri.add(Math.abs(k - adaySira));
const kaynakDizi = [...kaynakDegerleri];
const eksik = new Set<string>();
for (const s of sayilar(metin, false)) {
if (!s.ondalik && Number.isInteger(s.deger) && s.deger <= 31) continue;
if (!s.ondalik && s.deger >= 2015 && s.deger <= 2030) continue;
if (kaynakDegerleri.has(s.deger)) continue;
if (kaynakDizi.some((k) => yuvarlamaEslesir(k, s.deger))) continue;
eksik.add(s.ham);
}
return [...eksik];
}
export function icerikKontrolu(
metin: string,
kontrol: { icermeli?: string[]; icermeliBiri?: string[]; icermemeli?: string[] } | undefined,
): Kontrol[] {
if (!kontrol) return [];
const m = tr(metin);
const k: Kontrol[] = [];
const eksik = (kontrol.icermeli ?? []).filter((s) => !m.includes(tr(s)));
if (kontrol.icermeli) k.push({ ad: "icermeli", durum: eksik.length ? "kaldi" : "gecti", neden: eksik.length ? `eksik: ${eksik.join(" | ")}` : undefined });
if (kontrol.icermeliBiri) {
const var_ = kontrol.icermeliBiri.some((s) => m.includes(tr(s)));
k.push({ ad: "icermeliBiri", durum: var_ ? "gecti" : "kaldi", neden: var_ ? undefined : `hiçbiri yok: ${kontrol.icermeliBiri.join(" | ")}` });
}
const yasak = (kontrol.icermemeli ?? []).filter((s) => m.includes(tr(s)));
if (kontrol.icermemeli) k.push({ ad: "icermemeli", durum: yasak.length ? "kaldi" : "gecti", neden: yasak.length ? `geçiyor: ${yasak.join(" | ")}` : undefined });
return k;
}
// ───────────────────────── eval app DB'si ─────────────────────────
/**
* data/app.db'yi geçici klasöre kopyalar, göçleri uygular ve APP_DB_URL'i
* kopyaya çevirir. src/lib/appdb import EDİLMEDEN önce çağrılmalı.
*/
export function evalAppDbHazirla(): string {
const hedef = join(tmpdir(), `kolaytercih-eval-${process.pid}.db`);
copyFileSync(join(process.cwd(), "data", "app.db"), hedef);
process.env.APP_DB_URL = `file:${hedef}`;
delete process.env.APP_DB_AUTH_TOKEN;
execFileSync("node", ["scripts/db-goc.mjs"], {
env: { ...process.env, APP_DB_URL: `file:${hedef}` },
stdio: "ignore",
});
return hedef;
}

409
scripts/eval/robot.ts Normal file
View File

@@ -0,0 +1,409 @@
/**
* Tercih robotu (24'lük liste) eval'i.
*
* pnpm eval:robot # modelsiz değişmezler, 40 profil, saniyeler; ihlalde çıkış kodu 1
* pnpm eval:robot --model # + gerçek liste üretimi (profil başına 1–2 model çağrısı, PARA HARCAR)
* pnpm eval:robot --model --profil p05,a04 --limit 3 --eszaman 4 --butce 1
*
* Değişmezler `profiller.ts` içindeki `beklenen` ile kıyaslanır; bir değişmez
* bozuksa ve profilin `supheli` listesinde karşılığı (B1…B8) varsa "bilinen",
* yoksa "ihlal" sayılır. `beklenen` sapması her zaman ihlaldir.
*/
import {
argOku,
bayrakVar,
dayanaksizSayilar,
genelKarar,
gitDurumu,
havuzdaKos,
jevKontrolleri,
jevMaliyeti,
jevSor,
JEV_TOPLAM,
olcerekKos,
sonucYaz,
tablo,
TOPLAM,
vaatIfadeleri,
yasakSozcukler,
yuzdelik,
type Kontrol,
} from "./ortak";
import { JEV_EVAL_MODEL, JEV_SORU_GRUPLARI, type JevGerekceState } from "./jev-sorulari";
import { PROFILLER, type EvalProfil } from "./profiller";
import { getDb } from "@/lib/db";
import { kategoriEslesir } from "@/lib/kategoriler";
import { dilimHedefiHesapla, havuzOlustur, havuzuKompaktJson, turLabel, type AdayProgram, type DilimHedef } from "@/lib/rapor-havuzu";
import type { RaporParams, RaporSonuc } from "@/features/rapor/types/rapor";
const DILIMLER = ["hayal", "dengeli", "garanti"] as const;
const IDEAL: DilimHedef = { hayal: 5, dengeli: 13, garanti: 6 };
const hedefYaz = (h: DilimHedef) => `${h.hayal}/${h.dengeli}/${h.garanti}`;
const ayni = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b);
export const paramsYap = (p: EvalProfil): RaporParams => ({ sira: p.sihirbaz.sira, tur: p.sihirbaz.tur, ...p.sihirbaz.secimler });
type Ozellik = { min_basari_sirasi: number | null; ucret: number | null; burs_orani: string | null; ogrenim_dili: string | null };
function ozellikler(idler: string[]): Map<string, Ozellik> {
if (idler.length === 0) return new Map();
const satirlar = getDb()
.prepare(`SELECT id, min_basari_sirasi, ucret, burs_orani, ogrenim_dili FROM program_ozellik WHERE id IN (${idler.map(() => "?").join(",")})`)
.all(...idler) as (Ozellik & { id: string })[];
return new Map(satirlar.map((s) => [s.id, s]));
}
/** Gerekçede "X'da/de" diye geçen il adları (tam ad ya da yaygın kısa ad); program başka ildeyse gerekçe başka programa ait demektir. */
const IL_KISA: Record<string, string> = { afyon: "AFYONKARAHİSAR", maraş: "KAHRAMANMARAŞ", urfa: "ŞANLIURFA", antep: "GAZİANTEP", izmit: "KOCAELİ", adapazarı: "SAKARYA" };
let ilKumesi: Map<string, string> | null = null;
function gerekcedekiIller(metin: string): string[] {
if (!ilKumesi) {
const iller = (getDb().prepare("SELECT DISTINCT il FROM programs WHERE il IS NOT NULL").all() as { il: string }[]).map((r) => r.il);
ilKumesi = new Map([...iller.map((il) => [il.toLocaleLowerCase("tr-TR"), il] as [string, string]), ...Object.entries(IL_KISA)]);
}
const bulunan = new Set<string>();
// Yalnız BULUNMA ekiyle geçen il ("Konya'da", "Kayseri’de"): programın yerini iddia eder.
// "İzmir'e yakın" gibi yönelme hâli adayın kendi ilidir, çelişki değildir.
for (const e of metin.matchAll(/(?<![\p{L}])(\p{Lu}[\p{L}]+)['’][dt][ae](?:ki)?(?![\p{L}])/gu)) {
const il = ilKumesi.get(e[1].toLocaleLowerCase("tr-TR"));
if (il) bulunan.add(il);
}
return [...bulunan];
}
type Bulgu = { ad: string; durum: "ihlal" | "bilinen" | "duzeldi"; neden: string };
function degismezler(p: EvalProfil) {
const params = paramsYap(p);
const { havuz, gevsetildi } = havuzOlustur(params, p.danismanProfili);
const hedef = dilimHedefiHesapla(havuz);
const toplam = hedef.hayal + hedef.dengeli + hedef.garanti;
const oz = ozellikler(havuz.map((x) => x.id));
const s = p.sihirbaz.secimler;
const supheli = new Set(p.beklenen.supheli ?? []);
const barajDisi = havuz.filter((x) => {
const b = oz.get(x.id)?.min_basari_sirasi;
return b != null && b < params.sira;
});
const ozelKontenjan = havuz.filter((x) => /KKTC Uyruklu|M\.T\.O\.K\./.test(x.isim));
const alanIci = s.kategoriler.length ? havuz.filter((x) => kategoriEslesir(x.isim, s.kategoriler, x.fakulte)).length : havuz.length;
const ilIci = s.iller.length ? havuz.filter((x) => x.il != null && s.iller.includes(x.il)).length : havuz.length;
const tipIci =
s.universiteTipi === "farketmez"
? havuz.length
: havuz.filter((x) => (s.universiteTipi === "devlet" ? x.unitur === "DEVLET" : x.unitur !== "DEVLET")).length;
const kktcYurtdisi = havuz.filter((x) => /KKTC|YURTDISI/.test(x.unitur ?? "")).length;
const yinelenen = havuz.length - new Set(havuz.map((x) => x.id)).size;
const dilimSayisi = { hayal: 0, dengeli: 0, garanti: 0 };
for (const x of havuz) dilimSayisi[x.dilim]++;
const bulgular: Bulgu[] = [];
// 1) `beklenen` ile kıyas (bugünkü davranışın kaydı; sapma = davranış değişti)
if (!ayni(gevsetildi, p.beklenen.havuzGevsetmeBasamagi))
bulgular.push({ ad: "beklenen.gevsetme", durum: "ihlal", neden: `beklenen [${p.beklenen.havuzGevsetmeBasamagi}] gerçek [${gevsetildi}]` });
if (havuz.length < p.beklenen.minAday)
bulgular.push({ ad: "beklenen.minAday", durum: "ihlal", neden: `beklenen ≥${p.beklenen.minAday} gerçek ${havuz.length}` });
if (!ayni(hedef, p.beklenen.hedef))
bulgular.push({ ad: "beklenen.hedef", durum: "ihlal", neden: `beklenen ${hedefYaz(p.beklenen.hedef)} gerçek ${hedefYaz(hedef)}` });
// 2) Değişmezler; `supheli`deki B numarası varsa "bilinen"
const degismez = (ad: string, bozuk: boolean, bNo: string[], neden: string) => {
const isaretli = bNo.some((b) => supheli.has(b));
if (bozuk) bulgular.push({ ad, durum: isaretli ? "bilinen" : "ihlal", neden: `${neden}${isaretli ? ` (${bNo.filter((b) => supheli.has(b)).join(",")})` : ""}` });
else if (isaretli && bNo.every((b) => supheli.has(b))) bulgular.push({ ad, durum: "duzeldi", neden: `${bNo.join(",")} işaretli ama artık görülmüyor` });
};
degismez("liste-24", toplam < 24, ["B1", "B6"], `liste ${toplam} satır`);
degismez("baraj-disi", barajDisi.length > 0, ["B2"], `${barajDisi.length} program adayın yazamayacağı barajda (ör. ${barajDisi[0]?.isim} — ${barajDisi[0]?.universite})`);
degismez("ozel-kontenjan", ozelKontenjan.length > 0, ["B8"], `${ozelKontenjan.length} KKTC Uyruklu/M.T.O.K. satırı (ör. ${ozelKontenjan[0]?.isim} — ${ozelKontenjan[0]?.universite})`);
degismez("alan-ilsiz-gevsedi", gevsetildi.includes("kategori") && s.iller.length === 0, ["B3"], `il seçilmeden alan gevşedi; havuzda alan içi ${alanIci}/${havuz.length}`);
degismez("il-korunmadi", gevsetildi.includes("il") && ilIci < Math.min(3, toplam), ["B5"], `il gevşedi; havuzda seçili ilden ${ilIci}/${havuz.length}`);
if (tipIci < havuz.length) bulgular.push({ ad: "tip", durum: "ihlal", neden: `üniversite tipi dışı ${havuz.length - tipIci} program` });
if (yinelenen > 0) bulgular.push({ ad: "yinelenen", durum: "ihlal", neden: `${yinelenen} yinelenen program` });
return {
params,
havuz,
oz,
hedef,
satir: {
id: p.id,
gevsetildi,
havuz: havuz.length,
havuzDilim: dilimSayisi,
hedef,
liste: toplam,
ideal: ayni(hedef, IDEAL),
alanIci,
ilIci: s.iller.length ? ilIci : null,
tipIci,
barajDisi: barajDisi.length,
ozelKontenjan: ozelKontenjan.length,
kktcYurtdisi,
yinelenen,
bulgular,
},
};
}
// ───────────────────────── --model: gerçek liste üretimi ─────────────────────────
const KURTARMA_GEREKCESI = "Sıralamana ve seçimlerine uygun olduğu için listene eklendi.";
type HamListe = { hayal?: { programId?: string }[]; dengeli?: { programId?: string }[]; garanti?: { programId?: string }[] };
/** Modelin HAM çıktısı (normalize öncesi): rapor.ts havuz-dışını sessizce atar, burada sayılır. */
function hamDenetle(ham: string, havuzMap: Map<string, AdayProgram>, hedef: DilimHedef) {
let j: HamListe;
try {
j = JSON.parse(ham) as HamListe;
} catch {
return { semaGecerli: false, havuzDisi: 0, yinelenen: 0, yanlisDilim: 0, sayiTuttu: false };
}
const semaGecerli = DILIMLER.every((d) => Array.isArray(j[d]) && j[d]!.every((t) => typeof t?.programId === "string"));
if (!semaGecerli) return { semaGecerli, havuzDisi: 0, yinelenen: 0, yanlisDilim: 0, sayiTuttu: false };
let havuzDisi = 0;
let yinelenen = 0;
let yanlisDilim = 0;
const gorulen = new Set<string>();
for (const d of DILIMLER)
for (const t of j[d]!) {
const id = t.programId!;
const p = havuzMap.get(id);
if (!p) havuzDisi++;
else if (gorulen.has(id)) yinelenen++;
else if (p.dilim !== d) yanlisDilim++;
gorulen.add(id);
}
return { semaGecerli, havuzDisi, yinelenen, yanlisDilim, sayiTuttu: DILIMLER.every((d) => j[d]!.length === hedef[d]) };
}
async function modelleKos(p: EvalProfil, d: ReturnType<typeof degismezler>, raporUret: typeof import("@/lib/ai/rapor").raporUret) {
const havuzMap = new Map(d.havuz.map((x) => [x.id, x]));
const kompakt = new Map((JSON.parse(havuzuKompaktJson(d.havuz)) as { id: string }[]).map((x) => [x.id, JSON.stringify(x)]));
const t0 = Date.now();
let rapor: RaporSonuc | null = null;
let hata: string | null = null;
const { kullanim } = await olcerekKos(async () => {
try {
rapor = await raporUret(d.params, undefined, undefined, p.danismanProfili);
} catch (e) {
hata = e instanceof Error ? e.message.slice(0, 200) : String(e);
}
});
const ms = Date.now() - t0;
const ham = kullanim.hamIcerik.map((h) => hamDenetle(h, havuzMap, d.hedef));
const kontroller: Kontrol[] = [];
const temel = { id: p.id, ms, cagri: kullanim.cagri, girdiToken: kullanim.girdiToken, ciktiToken: kullanim.ciktiToken, maliyet: kullanim.maliyet, ham };
if (!rapor) {
return { ...temel, karar: "kaldi" as const, kontroller: [{ ad: "uretim", durum: "kaldi" as const, neden: hata ?? "rapor yok" }], satirlar: [], liste: null };
}
const r = rapor as RaporSonuc;
if (r.devMock) kontroller.push({ ad: "uretim", durum: "atlandi", neden: "anahtar yok: dev mock rapor (gerçek modelle koşulmadı)" });
// Ham çıktı (ilk deneme belirleyici: kullanıcı ikinci denemenin süresini bekler)
const ilk = ham[0];
kontroller.push({ ad: "sema", durum: !ilk || ilk.semaGecerli ? "gecti" : "kaldi", neden: ilk ? undefined : "ham çıktı yakalanamadı" });
const hd = ham.reduce((a, h) => a + h.havuzDisi, 0);
kontroller.push({ ad: "ham.havuz-disi", durum: hd ? "kaldi" : "gecti", neden: hd ? `${hd} havuz-dışı programId (normalize attı)` : undefined });
const hy = ham.reduce((a, h) => a + h.yinelenen, 0);
kontroller.push({ ad: "ham.yinelenen", durum: hy ? "gri" : "gecti", neden: hy ? `${hy} yinelenen programId` : undefined });
const yd = ham.reduce((a, h) => a + h.yanlisDilim, 0);
kontroller.push({ ad: "ham.yanlis-dilim", durum: yd ? "gri" : "gecti", neden: yd ? `${yd} program yanlış dilim listesinde` : undefined });
if (ham.length > 1) kontroller.push({ ad: "ikinci-deneme", durum: "gri", neden: `${ham.length} model denemesi gerekti` });
// Nihai liste (kullanıcının gördüğü)
const say = { hayal: 0, dengeli: 0, garanti: 0 };
for (const t of r.tercihler) say[t.dilim as keyof typeof say]++;
const beklenenToplam = d.hedef.hayal + d.hedef.dengeli + d.hedef.garanti;
kontroller.push({
ad: "satir-sayisi",
durum: r.tercihler.length === beklenenToplam && ayni(say, d.hedef) ? "gecti" : "kaldi",
neden: `${r.tercihler.length} satır ${hedefYaz(say)} (hedef ${hedefYaz(d.hedef)})`,
});
const disari = r.tercihler.filter((t) => !havuzMap.has(t.programId)).length;
kontroller.push({ ad: "havuz-disi", durum: disari ? "kaldi" : "gecti", neden: disari ? `${disari} satır havuz dışı` : undefined });
const yin = r.tercihler.length - new Set(r.tercihler.map((t) => t.programId)).size;
kontroller.push({ ad: "yinelenen", durum: yin ? "kaldi" : "gecti", neden: yin ? `${yin}` : undefined });
const kurtarma = r.tercihler.filter((t) => t.gerekce === KURTARMA_GEREKCESI).length;
if (kurtarma) kontroller.push({ ad: "kurtarma", durum: "gri", neden: `${kurtarma} satır kodla tamamlandı (model eksik döndü)` });
// Liste düzeyi metinler
const ustMetin = [r.genelDegerlendirme, ...(r.uyarilar ?? [])].join("\n");
const ustYasak = [...yasakSozcukler(ustMetin), ...vaatIfadeleri(ustMetin)];
kontroller.push({ ad: "ust-metin.yasak", durum: ustYasak.length ? "kaldi" : "gecti", neden: ustYasak.length ? ustYasak.join(" | ") : undefined });
// Satır gerekçeleri: yasak sözcük + sayı tutarlılığı (kod) + Jev gerekce grubu
const adayMetni = `${d.params.sira} ${JSON.stringify(p.sihirbaz.secimler)} ${JSON.stringify(p.danismanProfili ?? {})}`;
const satirlar = await Promise.all(
r.tercihler.map(async (t) => {
const prog = havuzMap.get(t.programId);
const o = d.oz.get(t.programId);
const yasak = [...new Set([...yasakSozcukler(t.gerekce), ...vaatIfadeleri(t.gerekce)])];
const sayi = prog ? dayanaksizSayilar(t.gerekce, [kompakt.get(t.programId) ?? "", adayMetni, JSON.stringify(o ?? {})]) : [];
// Üniversite adında geçen il (ör. "Ankara Üniversitesi" İstanbul'da olmasa da) çelişki sayılmaz
const yanlisIl = prog ? gerekcedekiIller(t.gerekce).filter((il) => il !== prog.il && !prog.universite.includes(il) && !prog.isim.toLocaleUpperCase("tr-TR").includes(il)) : [];
const state: JevGerekceState = {
metin: t.gerekce,
program: {
ad: prog?.isim ?? null,
universite: prog?.universite ?? null,
il: prog?.il ?? null,
tur: prog?.unitur ?? null,
dil: o?.ogrenim_dili ?? null,
burs: o?.burs_orani ?? null,
ucret: o?.ucret ?? null,
taban_sirasi: prog?.efektifSira ?? null,
kontenjan: prog?.kontenjanSon ?? null,
dilim: t.dilim === "garanti" ? "güvenli" : t.dilim,
},
aday: { sira: d.params.sira, puan_turu: turLabel(d.params.tur) },
};
const jev = t.gerekce === KURTARMA_GEREKCESI || r.devMock ? null : await jevSor(state, JEV_SORU_GRUPLARI.gerekce);
const k: Kontrol[] = [
{ ad: "yasak-ifade", durum: yasak.length ? "kaldi" : "gecti", neden: yasak.length ? yasak.join(" | ") : undefined },
{ ad: "il-celiski", durum: yanlisIl.length ? "kaldi" : "gecti", neden: yanlisIl.length ? `gerekçe ${yanlisIl.join(", ")} diyor, program ${prog?.il}` : undefined },
{ ad: "sayi", durum: sayi.length ? "gri" : "gecti", neden: sayi.length ? `kaynakta yok: ${sayi.join(", ")}` : undefined },
...(jev ? jevKontrolleri(jev, null) : []),
];
return { sira: t.sira, programId: t.programId, dilim: t.dilim, program: prog ? `${prog.isim} — ${prog.universite}` : "?", taban: prog?.efektifSira ?? null, gerekce: t.gerekce, jev: jev?.durum === "ok" ? jev.p : null, karar: genelKarar(k), kontroller: k.filter((x) => x.durum !== "gecti") };
}),
);
const kalan = satirlar.filter((s) => s.karar === "kaldi").length;
const gri = satirlar.filter((s) => s.karar === "gri").length;
kontroller.push({ ad: "gerekceler", durum: kalan ? "kaldi" : gri ? "gri" : "gecti", neden: `${kalan} kaldı, ${gri} gri / ${satirlar.length}` });
return {
...temel,
karar: genelKarar(kontroller),
kontroller,
satirlar,
// Sohbet eval'i bağlam olarak bunu kullanır (yalnız seçilen programlar; havuzun tamamı değil)
liste: {
tercihler: r.tercihler,
genelDegerlendirme: r.genelDegerlendirme,
uyarilar: r.uyarilar,
programlar: Object.fromEntries(r.tercihler.map((t) => [t.programId, r.programlar[t.programId]])),
kapsam: r.kapsam,
devMock: r.devMock,
} satisfies RaporSonuc,
};
}
// ───────────────────────── ana akış ─────────────────────────
async function main() {
const profilSuzgec = argOku("profil")?.split(",").filter(Boolean);
const limit = Number(argOku("limit")) || Infinity;
const profiller = PROFILLER.filter((p) => !profilSuzgec || profilSuzgec.some((s) => p.id.startsWith(s))).slice(0, limit);
const t0 = Date.now();
const det = profiller.map((p) => ({ p, d: degismezler(p) }));
const detMs = Date.now() - t0;
console.log(`\n== Değişmezler (modelsiz) — ${profiller.length} profil, ${detMs} ms ==\n`);
console.log(
tablo(
["profil", "gevşetilen", "havuz", "hedef", "liste", "alan", "il", "baraj", "özelK", "sonuç"],
det.map(({ d }) => {
const s = d.satir;
const ihlal = s.bulgular.filter((b) => b.durum === "ihlal").length;
const bilinen = s.bulgular.filter((b) => b.durum === "bilinen").length;
return [s.id.slice(0, 44), s.gevsetildi.join("→") || "—", s.havuz, hedefYaz(s.hedef), s.liste, `${s.alanIci}/${s.havuz}`, s.ilIci == null ? "–" : `${s.ilIci}/${s.havuz}`, s.barajDisi, s.ozelKontenjan, ihlal ? `İHLAL×${ihlal}` : bilinen ? `bilinen×${bilinen}` : "temiz"];
}),
),
);
const ihlalli = det.filter(({ d }) => d.satir.bulgular.some((b) => b.durum === "ihlal"));
const bilinenli = det.filter(({ d }) => !d.satir.bulgular.some((b) => b.durum === "ihlal") && d.satir.bulgular.some((b) => b.durum === "bilinen"));
for (const { d } of det)
for (const b of d.satir.bulgular) console.log(` [${b.durum}] ${d.satir.id}: ${b.ad} — ${b.neden}`);
const detOzet = {
profil: det.length,
temiz: det.length - ihlalli.length - bilinenli.length,
yalnizBilinen: bilinenli.length,
ihlalli: ihlalli.length,
liste24: det.filter(({ d }) => d.satir.liste === 24).length,
ideal: det.filter(({ d }) => d.satir.ideal).length,
barajDisiProfil: det.filter(({ d }) => d.satir.barajDisi > 0).length,
ozelKontenjanProfil: det.filter(({ d }) => d.satir.ozelKontenjan > 0).length,
ms: detMs,
};
console.log(`\nÖzet: ${detOzet.temiz} temiz · ${detOzet.yalnizBilinen} yalnız bilinen bug · ${detOzet.ihlalli} ihlalli | 24 satır: ${detOzet.liste24}/${det.length} · 5/13/6: ${detOzet.ideal}/${det.length}`);
let model: unknown = null;
if (bayrakVar("model")) {
const { raporUret } = await import("@/lib/ai/rapor");
const { raporModeli, saglayiciAdi, anahtarVar } = await import("@/lib/ai/client");
const butce = Number(argOku("butce")) || 1;
const eszaman = Number(argOku("eszaman")) || 4;
console.log(`\n== Gerçek liste üretimi — ${det.length} profil, model ${raporModeli()} @ ${saglayiciAdi()}, eşzamanlılık ${eszaman}, bütçe ${butce} $ ==`);
if (!anahtarVar()) console.log("UYARI: model anahtarı yok — dev mock rapor dönecek; sonuçlar 'gerçek modelle koşulmadı'.");
const sonuclar = (
await havuzdaKos(
det,
eszaman,
async ({ p, d }) => {
const s = await modelleKos(p, d, raporUret);
console.log(` ${s.karar.padEnd(6)} ${p.id} ${(s.ms / 1000).toFixed(1)} sn, ${s.cagri} çağrı, ${s.maliyet == null ? "?" : s.maliyet.toFixed(4)} $ ${s.kontroller.filter((k) => k.durum !== "gecti").map((k) => `${k.ad}${k.neden ? `(${k.neden})` : ""}`).join("; ")}`);
return s;
},
() => TOPLAM.maliyet >= butce,
)
).filter((s) => s != null);
const sureler = sonuclar.map((s) => s.ms);
const say = (k: string) => sonuclar.filter((s) => s.karar === k).length;
const kontrolKalan = (ad: string) => sonuclar.filter((s) => s.kontroller.some((k) => k.ad === ad && k.durum === "kaldi")).length;
const tumSatir = sonuclar.flatMap((s) => s.satirlar);
const ozet = {
model: raporModeli(),
saglayici: saglayiciAdi(),
jevModel: JEV_EVAL_MODEL,
profil: sonuclar.length,
atlananProfil: det.length - sonuclar.length,
gecti: say("gecti"),
gri: say("gri"),
kaldi: say("kaldi"),
semaGecersiz: kontrolKalan("sema"),
uretimHatasi: kontrolKalan("uretim"),
hamHavuzDisiProgram: sonuclar.reduce((a, s) => a + s.ham.reduce((b, h) => b + h.havuzDisi, 0), 0),
ikinciDenemeGereken: sonuclar.filter((s) => s.ham.length > 1).length,
satirSayisiTutmayan: kontrolKalan("satir-sayisi"),
nihaiHavuzDisi: kontrolKalan("havuz-disi"),
gerekce: {
toplam: tumSatir.length,
yasakIfade: tumSatir.filter((s) => s.kontroller.some((k) => k.ad === "yasak-ifade")).length,
ilCeliski: tumSatir.filter((s) => s.kontroller.some((k) => k.ad === "il-celiski")).length,
sayiAdayi: tumSatir.filter((s) => s.kontroller.some((k) => k.ad === "sayi")).length,
jevIhlal: tumSatir.filter((s) => s.kontroller.some((k) => k.ad.startsWith("jev:") && k.durum === "kaldi")).length,
jevGri: tumSatir.filter((s) => s.kontroller.some((k) => k.ad.startsWith("jev:") && k.durum === "gri")).length,
jevAtlanan: tumSatir.filter((s) => s.kontroller.some((k) => k.ad === "jev" && k.durum === "atlandi")).length,
},
gecikmeMs: { p50: yuzdelik(sureler, 50), p95: yuzdelik(sureler, 95), max: Math.max(0, ...sureler) },
harcama: { ...TOPLAM, jev: { ...JEV_TOPLAM, maliyet: jevMaliyeti() } },
};
console.log(`\nÖzet (model): ${ozet.gecti} geçti · ${ozet.gri} gri · ${ozet.kaldi} kaldı / ${ozet.profil}${ozet.atlananProfil ? ` (bütçe freni: ${ozet.atlananProfil} profil koşulmadı)` : ""}`);
console.log(` ham havuz-dışı program: ${ozet.hamHavuzDisiProgram} · şema geçersiz: ${ozet.semaGecersiz} · 2. deneme: ${ozet.ikinciDenemeGereken} · satır sayısı tutmayan: ${ozet.satirSayisiTutmayan}`);
console.log(` gerekçe ${ozet.gerekce.toplam}: yasak ifade ${ozet.gerekce.yasakIfade} · il çelişkisi ${ozet.gerekce.ilCeliski} · sayı adayı ${ozet.gerekce.sayiAdayi} · Jev ihlal ${ozet.gerekce.jevIhlal} · Jev gri ${ozet.gerekce.jevGri} · Jev atlanan ${ozet.gerekce.jevAtlanan}`);
console.log(` gecikme p50 ${ozet.gecikmeMs.p50} ms · p95 ${ozet.gecikmeMs.p95} ms`);
console.log(` harcama: ${TOPLAM.cagri} model çağrısı, ${TOPLAM.girdiToken} girdi + ${TOPLAM.ciktiToken} çıktı token, ${TOPLAM.maliyet.toFixed(4)} $${TOPLAM.maliyetBilinmeyenCagri ? ` (+${TOPLAM.maliyetBilinmeyenCagri} çağrının maliyeti bildirilmedi)` : ""}; Jev ${JEV_TOPLAM.cagri} çağrı ≈ ${jevMaliyeti().toFixed(4)} $`);
// Dosya küçük kalsın: geçen satırların gerekçesi zaten `liste.tercihler` içinde
model = { ozet, profiller: sonuclar.map((s) => ({ ...s, satirSayisi: s.satirlar.length, satirlar: s.satirlar.filter((x) => x.karar !== "gecti") })) };
}
// Modelsiz koşu (CI) dosya bırakmaz; taban çizgisi için --kaydet
if (!bayrakVar("model") && !bayrakVar("kaydet")) process.exit(ihlalli.length > 0 ? 1 : 0);
const yol = sonucYaz("robot", {
tur: "robot",
tarih: new Date().toISOString(),
git: gitDurumu(),
argv: process.argv.slice(2),
degismezler: { ozet: detOzet, profiller: det.map(({ d }) => d.satir) },
model,
});
console.log(`\nHam sonuç: ${yol}`);
process.exit(ihlalli.length > 0 ? 1 : 0);
}
main().catch((e) => {
console.error(e);
process.exit(2);
});

363
scripts/eval/sohbet.ts Normal file
View File

@@ -0,0 +1,363 @@
/**
* Danışman sohbeti eval'i — gerçek danismanAkisi (11 araç) ile. PARA HARCAR.
*
* pnpm eval:sohbet # 112 soru × 1
* pnpm eval:sohbet --limit 10 # ilk 10 (süzgeçlerden sonra)
* pnpm eval:sohbet --etiket vaat,kriz --profil p05 --soru s001,s035
* pnpm eval:sohbet --eszaman 5 --butce 1.5 --liste scripts/eval/sonuc/<robot>.json
*
* Bağlam route.ts'in kurduğu biçimde kurulur: sistemPromptu + listeOzetiCikar
* + modelPenceresi + aracBaglami aynı modüllerden import edilir. Tek kopya:
* kilitli (paketsiz) özetin ilk ACIK_SATIR satıra kırpılması (route.ts içinde,
* dışa aktarılmamış). Liste: en yeni `--model` robot koşusundan; yoksa havuzdan
* deterministik (model çağrısı yapılmaz).
*
* profil_guncelle gerçek kodla çalışır ama data/app.db'nin GEÇİCİ KOPYASINA
* yazar; her soru kendi sahte kullanıcısıyla koşar, yazılan profil geri okunur.
*/
import { readFileSync } from "node:fs";
import Database from "better-sqlite3";
import {
argOku,
dayanaksizSayilar,
evalAppDbHazirla,
genelKarar,
gitDurumu,
havuzdaKos,
icerikKontrolu,
jevKontrolleri,
jevMaliyeti,
jevSor,
JEV_TOPLAM,
kisalt,
olcerekKos,
robotSonuclari,
sonucYaz,
tablo,
TOPLAM,
vaatIfadeleri,
yuzdelik,
type JevSonuc,
type Karar,
type Kontrol,
} from "./ortak";
import { JEV_EVAL_MODEL, JEV_SORU_GRUPLARI, type JevDanismanState, type JevSoruAdi } from "./jev-sorulari";
import { PROFILLER, SORULAR, type EvalProfil, type EvalSoru } from "./profiller";
import type { RaporParams, RaporSonuc } from "@/features/rapor/types/rapor";
const paramsYap = (p: EvalProfil): RaporParams => ({ sira: p.sihirbaz.sira, tur: p.sihirbaz.tur, ...p.sihirbaz.secimler });
type AracCagrisi = { ad: string; girdi: unknown; cikti: unknown; hata?: string };
async function main() {
// Ortam ÖNCE: appdb modülü import anında bağlanır
const dbYolu = evalAppDbHazirla();
const sqlite = new Database(dbYolu);
const { danismanAkisi } = await import("@/lib/ai/sohbet-akisi");
const { sistemPromptu } = await import("@/lib/ai/danisman-prompt");
const { modelPenceresi } = await import("@/lib/ai/sohbet-gecmisi");
const { listeOzetiCikar } = await import("@/lib/ai/rapor");
const { sohbetModeli, saglayiciAdi, anahtarVar } = await import("@/lib/ai/client");
const { JEV_DUZELTME_NOTU } = await import("@/lib/ai/jev");
const { ACIK_SATIR } = await import("@/lib/rapor-maske");
const { havuzOlustur, dilimHedefiHesapla } = await import("@/lib/rapor-havuzu");
const { PUAN_TURLERI } = await import("@/types/yokatlas");
if (!anahtarVar()) {
console.error("Model anahtarı yok (AI_API_KEY / OPENROUTER_API_KEY): sohbet eval'i gerçek modelle koşulamadı.");
process.exit(2);
}
// ── soru seçimi ──
const liste = (ad: string) => argOku(ad)?.split(",").filter(Boolean);
const etiketS = liste("etiket");
const profilS = liste("profil");
const soruS = liste("soru");
const limit = Number(argOku("limit")) || Infinity;
const eszaman = Number(argOku("eszaman")) || 5;
const butce = Number(argOku("butce")) || 1.5;
const sorular = SORULAR.filter(
(s) =>
(!etiketS || s.etiketler.some((e) => etiketS.includes(e))) &&
(!profilS || profilS.some((p) => s.profilId.startsWith(p))) &&
(!soruS || soruS.includes(s.id)),
).slice(0, limit);
// ── liste bağlamı ──
// --liste verilmezse: gerçek model listesi içeren en yeni robot sonucu
const robotListeleri = new Map<string, RaporSonuc>();
const robotOku = (yol: string) => {
const j = JSON.parse(readFileSync(yol, "utf8")) as { model?: { profiller?: { id: string; liste: RaporSonuc | null }[] } };
for (const p of j.model?.profiller ?? []) if (p.liste && !p.liste.devMock) robotListeleri.set(p.id, p.liste);
};
let robotYolu = argOku("liste") || undefined;
if (robotYolu) robotOku(robotYolu);
else
for (const yol of robotSonuclari()) {
robotOku(yol);
if (robotListeleri.size) {
robotYolu = yol;
break;
}
}
const listeKaynagi = new Map<string, "robot" | "deterministik">();
const raporlar = new Map<string, RaporSonuc>();
const raporGetir = (p: EvalProfil): RaporSonuc => {
const var_ = raporlar.get(p.id);
if (var_) return var_;
let r = robotListeleri.get(p.id);
listeKaynagi.set(p.id, r ? "robot" : "deterministik");
if (!r) {
// Model çağrısı yok: her dilimden taban sırası en iyi `hedef` kadar program
const { havuz } = havuzOlustur(paramsYap(p), p.danismanProfili);
const hedef = dilimHedefiHesapla(havuz);
const secilen = (["hayal", "dengeli", "garanti"] as const).flatMap((d) =>
havuz.filter((x) => x.dilim === d).sort((a, b) => (a.efektifSira ?? 0) - (b.efektifSira ?? 0)).slice(0, hedef[d]),
);
r = {
tercihler: secilen.map((x, i) => ({ programId: x.id, sira: i + 1, dilim: x.dilim, gerekce: "", riskNotu: "", trendOzeti: "" })),
genelDegerlendirme: "",
programlar: Object.fromEntries(secilen.map((x) => [x.id, { isim: x.isim, universite: x.universite, il: x.il, unitur: x.unitur, efektifSira: x.efektifSira }])),
} as RaporSonuc;
}
raporlar.set(p.id, r);
return r;
};
const now = Date.now();
const kullaniciEkle = sqlite.prepare("INSERT OR REPLACE INTO user (id, name, email, email_verified, credit_balance, has_paket, created_at, updated_at) VALUES (?, 'Eval', ?, 1, 0, 1, ?, ?)");
const profilEkle = sqlite.prepare("INSERT OR REPLACE INTO danisman_profil (user_id, alanlar, updated_at) VALUES (?, ?, ?)");
const profilOku = sqlite.prepare("SELECT alanlar FROM danisman_profil WHERE user_id = ?");
console.log(`\n== Sohbet eval — ${sorular.length} soru, model ${sohbetModeli()} @ ${saglayiciAdi()}, eşzamanlılık ${eszaman}, bütçe ${butce} $ ==`);
console.log(`Liste bağlamı: ${robotYolu && robotListeleri.size ? `${robotListeleri.size} profil ${robotYolu.split("/").pop()} dosyasından` : "deterministik (robot --model sonucu yok)"}\n`);
// src tarafındaki "[soru] araç:" / "[soru] jev" satırları özeti boğmasın
const asilInfo = console.info;
console.info = () => {};
async function soruKos(s: EvalSoru) {
const p = PROFILLER.find((x) => x.id === s.profilId);
if (!p) throw new Error(`Profil yok: ${s.profilId}`);
const params = paramsYap(p);
const rapor = raporGetir(p);
const kilitli = p.paketli === false;
const tamOzet = listeOzetiCikar(rapor, params.sira);
// route.ts ile aynı kırpma (dışa aktarılmamış; "24 - ACIK_SATIR" sabiti dahil birebir)
const listeOzeti = kilitli ? [...tamOzet.split("\n").slice(0, ACIK_SATIR), `… kalan ${24 - ACIK_SATIR} tercih kilitli.`].join("\n") : tamOzet;
const profil = { ...(p.danismanProfili ?? {}) };
const system = sistemPromptu({ params, listeOzeti, kilitli, profil });
const gecmis = (s.gecmis ?? []).map((m) => ({ role: m.rol, content: m.icerik }));
const mesajlar = modelPenceresi(gecmis, s.soru);
const userId = `eval-${process.pid}-${s.id}`;
kullaniciEkle.run(userId, `${userId}@eval.invalid`, now, now);
if (p.danismanProfili) profilEkle.run(userId, JSON.stringify(p.danismanProfili), now);
const araclar: AracCagrisi[] = [];
const girdiler = new Map<string, AracCagrisi>();
let adim = 0;
let ilkTokenMs: number | null = null;
let tamMetin: string | null = null;
let yarimMetin: string | null = null;
let akisHatasi: string | null = null;
const t0 = Date.now();
const { kullanim } = await olcerekKos(async () => {
try {
const okuyucu = danismanAkisi({
system,
mesajlar,
aracBaglami: { userId, sira: params.sira, tur: params.tur, profil },
onTamam: async (m) => {
tamMetin = m;
},
onYarim: async (m) => {
yarimMetin = m;
},
}).getReader();
for (;;) {
const { done, value: parca } = await okuyucu.read();
if (done) break;
const v = parca as { type: string; toolCallId?: string; toolName?: string; input?: unknown; output?: unknown; errorText?: string };
if (v.type === "start-step") adim++;
else if (v.type === "text-delta" && ilkTokenMs == null) ilkTokenMs = Date.now() - t0;
else if (v.type === "tool-input-available") {
const c: AracCagrisi = { ad: v.toolName ?? "?", girdi: v.input, cikti: undefined };
araclar.push(c);
if (v.toolCallId) girdiler.set(v.toolCallId, c);
} else if (v.type === "tool-output-available" && v.toolCallId) {
const c = girdiler.get(v.toolCallId);
if (c) c.cikti = v.output;
} else if (v.type === "tool-input-error" || v.type === "tool-output-error") {
const c = v.toolCallId ? girdiler.get(v.toolCallId) : undefined;
if (c) c.hata = v.errorText ?? v.type;
else araclar.push({ ad: v.toolName ?? "?", girdi: v.input, cikti: undefined, hata: v.errorText ?? v.type });
} else if (v.type === "error") akisHatasi = v.errorText ?? "hata";
}
} catch (e) {
akisHatasi = e instanceof Error ? e.message : String(e);
}
});
const toplamMs = Date.now() - t0;
const kayitliProfil = (() => {
const satir = profilOku.get(userId) as { alanlar: string } | undefined;
try {
return satir ? (JSON.parse(satir.alanlar) as Record<string, unknown>) : {};
} catch {
return {};
}
})();
const hamMetin: string = tamMetin ?? yarimMetin ?? "";
const canliJevNotu = hamMetin.includes(JEV_DUZELTME_NOTU);
const metin = hamMetin.replace(JEV_DUZELTME_NOTU, "").trim();
const m = metin.toLocaleLowerCase("tr-TR");
const cagrilan = araclar.map((a) => a.ad);
const kontroller: Kontrol[] = [];
// 0) akış
if (tamMetin == null) kontroller.push({ ad: "akis", durum: "kaldi", neden: `cevap tamamlanmadı: ${akisHatasi ?? "?"}` });
// 1) araç kuralları
if (s.beklenenAraclar.length) {
const eksik = s.beklenenAraclar.filter((a) => !cagrilan.includes(a));
const ok = s.aracKurali === "herhangi" ? eksik.length < s.beklenenAraclar.length : eksik.length === 0;
kontroller.push({ ad: "beklenen-arac", durum: ok ? "gecti" : "kaldi", neden: ok ? undefined : `çağrılmadı: ${eksik.join(", ")} (çağrılan: ${cagrilan.join(", ") || "yok"})` });
}
const yasak = (s.yasakAraclar ?? []).filter((a) => cagrilan.includes(a));
if (s.yasakAraclar) kontroller.push({ ad: "yasak-arac", durum: yasak.length ? "kaldi" : "gecti", neden: yasak.length ? `çağrıldı: ${yasak.join(", ")}` : undefined });
const aracHatalari = araclar.filter((a) => a.hata);
if (aracHatalari.length) kontroller.push({ ad: "arac-hatasi", durum: "gri", neden: aracHatalari.map((a) => `${a.ad}: ${kisalt(a.hata ?? "", 80)}`).join("; ") });
// 2) metin kontrolü
kontroller.push(...icerikKontrolu(metin, s.kontrol));
// 3) "kaydettim" deyip kaydetmeme (H4): başarılı profil_guncelle çıktısı yoksa
const basariliKayit = araclar.filter((a) => a.ad === "profil_guncelle" && a.cikti != null && typeof a.cikti === "object" && "kaydedildi" in (a.cikti as object)).length;
if (/kaydettim|kaydedildi|profiline ekledim|profilini güncelledim/.test(m)) {
kontroller.push({ ad: "kaydettim-ama-yazmadi", durum: basariliKayit ? "gecti" : "kaldi", neden: basariliKayit ? undefined : "cevap kayıt bildiriyor, başarılı profil_guncelle çağrısı yok" });
}
// 4) vaat regex'i
const vaat = vaatIfadeleri(metin);
// 5) sayılar: araç çıktısı / liste / profil / soru / geçmiş / araç girdisi
const kaynaklar = [system, s.soru, ...gecmis.map((g) => g.content), ...araclar.map((a) => JSON.stringify(a.girdi ?? "")), ...araclar.map((a) => JSON.stringify(a.cikti ?? ""))];
const sayi = metin ? dayanaksizSayilar(metin, kaynaklar, params.sira) : [];
if (sayi.length) kontroller.push({ ad: "sayi", durum: "gri", neden: `kaynakta yok: ${sayi.join(", ")}` });
// 6) Jev danışman grubu (tek çağrı). Kullanıcı mesajı GİTMEZ.
let jev: JevSonuc | null = null;
if (metin) {
const il = p.danismanProfili?.evIl ?? null;
const jevSorulari: JevSoruAdi[] = JEV_SORU_GRUPLARI.danisman.filter((q) => q !== "profil_il" || il != null);
let kalan = 24_000;
const aracCiktisi = araclar
.filter((a) => a.cikti != null)
.map((a) => {
const j = kisalt(JSON.stringify(a.cikti), Math.min(8000, Math.max(0, kalan)));
kalan -= j.length;
return { arac: a.ad, girdi: a.girdi, cikti: j };
});
const state: JevDanismanState = {
metin,
arac_ciktisi: aracCiktisi.length ? aracCiktisi : "yok",
liste: listeOzeti.split("\n"),
profil: { sira: params.sira, puan_turu: PUAN_TURLERI[params.tur], il },
};
jev = await jevSor(state, jevSorulari);
const kriz = s.etiketler.includes("kriz") ? "kriz" : s.etiketler.includes("kriz-degil") ? "kriz-degil" : null;
kontroller.push(...jevKontrolleri(jev, kriz));
}
const jevVaatP = jev?.durum === "ok" ? (jev.p.yerlesme_vaadi ?? 0) : 0;
if (vaat.length) kontroller.push({ ad: "vaat-regex", durum: jevVaatP >= 0.35 ? "kaldi" : "gri", neden: vaat.join(" | ") });
const karar: Karar = genelKarar(kontroller);
const sorunlar = kontroller.filter((k) => k.durum !== "gecti");
asilInfo(` ${karar.padEnd(6)} ${s.id} [${s.etiketler.join(",")}] ${(toplamMs / 1000).toFixed(1)} sn, ${adim} adım, araç: ${cagrilan.join("+") || "—"}${sorunlar.length ? ` ⇒ ${sorunlar.map((k) => `${k.ad}${k.neden ? `(${kisalt(k.neden, 90)})` : ""}`).join("; ")}` : ""}`);
return {
id: s.id,
profilId: s.profilId,
etiketler: s.etiketler,
soru: s.soru,
gecmisVar: gecmis.length > 0,
beklenenAraclar: s.beklenenAraclar,
beklenenDavranis: s.beklenenDavranis,
listeKaynagi: listeKaynagi.get(p.id),
karar,
kontroller: sorunlar,
araclar: araclar.map((a) => ({ ad: a.ad, girdi: a.girdi, hata: a.hata, cikti: a.cikti == null ? null : kisalt(JSON.stringify(a.cikti), 1500) })),
adim,
cevap: metin,
canliJevNotu,
kayitliProfil,
jev: jev?.durum === "ok" ? jev.p : jev ? { atlandi: jev.neden } : null,
ilkTokenMs: ilkTokenMs as number | null,
toplamMs,
modelCagrisi: kullanim.cagri,
girdiToken: kullanim.girdiToken,
ciktiToken: kullanim.ciktiToken,
maliyet: kullanim.maliyet,
};
}
const sonuclar = (await havuzdaKos(sorular, eszaman, soruKos, () => TOPLAM.maliyet >= butce)).filter((x) => x != null);
console.info = asilInfo;
// ── özet ──
const oran = (d: typeof sonuclar) => {
const g = d.filter((x) => x.karar === "gecti").length;
const gri = d.filter((x) => x.karar === "gri").length;
const k = d.filter((x) => x.karar === "kaldi").length;
return { n: d.length, gecti: g, gri, kaldi: k, gecmeOrani: d.length ? Math.round((100 * g) / d.length) : 0 };
};
const grupla = (anahtarlar: (x: (typeof sonuclar)[number]) => string[]) => {
const g = new Map<string, typeof sonuclar>();
for (const x of sonuclar) for (const a of anahtarlar(x)) g.set(a, [...(g.get(a) ?? []), x]);
return Object.fromEntries([...g].sort((a, b) => b[1].length - a[1].length).map(([a, d]) => [a, oran(d)]));
};
const etiketBasina = grupla((x) => x.etiketler);
const aracBasina = grupla((x) => x.beklenenAraclar);
const kontrolBasina: Record<string, { kaldi: number; gri: number; atlandi: number }> = {};
for (const x of sonuclar)
for (const k of x.kontroller) {
const o = (kontrolBasina[k.ad] ??= { kaldi: 0, gri: 0, atlandi: 0 });
if (k.durum !== "gecti") o[k.durum]++;
}
const sure = sonuclar.map((x) => x.toplamMs);
const ilk = sonuclar.map((x) => x.ilkTokenMs).filter((x): x is number => x != null);
const ozet = {
model: sohbetModeli(),
saglayici: saglayiciAdi(),
jevModel: JEV_EVAL_MODEL,
...oran(sonuclar),
atlananSoru: sorular.length - sonuclar.length,
canliJevNotuEklenen: sonuclar.filter((x) => x.canliJevNotu).length,
ortalamaAdim: sonuclar.length ? +(sonuclar.reduce((a, x) => a + x.adim, 0) / sonuclar.length).toFixed(2) : 0,
gecikmeMs: { ilkToken: { p50: yuzdelik(ilk, 50), p95: yuzdelik(ilk, 95) }, toplam: { p50: yuzdelik(sure, 50), p95: yuzdelik(sure, 95), max: Math.max(0, ...sure) } },
harcama: { ...TOPLAM, jev: { ...JEV_TOPLAM, maliyet: jevMaliyeti() } },
etiketBasina,
aracBasina,
kontrolBasina,
};
const satir = (o: Record<string, ReturnType<typeof oran>>) => Object.entries(o).map(([a, v]) => [a, v.n, v.gecti, v.gri, v.kaldi, `%${v.gecmeOrani}`]);
console.log(`\nÖzet: ${ozet.gecti} geçti · ${ozet.gri} gri · ${ozet.kaldi} kaldı / ${ozet.n} (%${ozet.gecmeOrani})${ozet.atlananSoru ? ` — bütçe freni: ${ozet.atlananSoru} soru koşulmadı` : ""}`);
console.log(`\n${tablo(["etiket", "n", "geçti", "gri", "kaldı", "geçme"], satir(etiketBasina))}`);
console.log(`\n${tablo(["beklenen araç", "n", "geçti", "gri", "kaldı", "geçme"], satir(aracBasina))}`);
console.log(`\n${tablo(["kontrol", "kaldı", "gri", "atlandı"], Object.entries(kontrolBasina).sort((a, b) => b[1].kaldi - a[1].kaldi).map(([a, v]) => [a, v.kaldi, v.gri, v.atlandi]))}`);
console.log(`\nGecikme: ilk token p50 ${ozet.gecikmeMs.ilkToken.p50} ms / p95 ${ozet.gecikmeMs.ilkToken.p95} ms · toplam p50 ${ozet.gecikmeMs.toplam.p50} ms / p95 ${ozet.gecikmeMs.toplam.p95} ms · ortalama ${ozet.ortalamaAdim} adım`);
console.log(`Harcama: ${TOPLAM.cagri} model çağrısı, ${TOPLAM.girdiToken} girdi + ${TOPLAM.ciktiToken} çıktı token, ${TOPLAM.maliyet.toFixed(4)} $${TOPLAM.maliyetBilinmeyenCagri ? ` (+${TOPLAM.maliyetBilinmeyenCagri} çağrının maliyeti bildirilmedi)` : ""}; Jev hakem ${JEV_TOPLAM.cagri} çağrı ≈ ${jevMaliyeti().toFixed(4)} $ (${JEV_TOPLAM.atlanan} atlandı); ayrıca canlı Jev denetimi cevap başına 1 çağrı (akışın içinde, ölçülmedi)`);
const yol = sonucYaz("sohbet", { tur: "sohbet", tarih: new Date().toISOString(), git: gitDurumu(), argv: process.argv.slice(2), listeDosyasi: robotYolu?.split("/").pop() ?? null, ozet, sorular: sonuclar });
console.log(`\nHam sonuç: ${yol}`);
sqlite.close();
process.exit(0);
}
main().catch((e) => {
console.error(e);
process.exit(2);
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff