test(eval): revizyon evali koda bağlı yola göre — niyet/kural/dal/şablon ölçümü; akis-denetim'e 4 koddan senaryo

- eval:revizyon: regex kapısı + yapılandırılmış çıkarım + koddan işlem (sahte
  çekirdek) ölçülür; beklenen kural alt kümesi, sahte beyan, ad eşleşme, niyet
  gecikmesi/maliyeti ayrı. Ham sonuçlar: 22-57 (duman), 22-59 (model anlatımı,
  2/8 yanlış sayı), 23-02 (doğal kayıt), 23-07 (şablon; r04 niyet zaman aşımı
  → güvenlik ağı), 23-08 (r04 tekrar).
- akis-denetim (13/13): koddan revizyon (modelsiz şablon), niyet=diger + sahte
  beyan → not + iade, koddan form (model çağrısı 0), koddan revizyon hatası.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-09 02:10:59 +03:00
parent bad5856a52
commit e9dfa354f8
7 changed files with 1564 additions and 64 deletions

View File

@@ -1,38 +1,39 @@
/**
* Revizyon isteğinde danışmanın `liste_revize` / `soru_sor` aracını ÇAĞIRIP
* ÇAĞIRMADIĞININ ölçümü — gerçek modelle, PARA HARCAR (senaryo başına 1-4
* model çağrısı; 8 Eki 2026 koşusu: 10 senaryo ≈ 25 çağrı ≈ 0,02 $).
* KODA BAĞLI liste revizyonunun ölçümü — gerçek modelle, PARA HARCAR
* (revizyon senaryosunda yalnız 1 niyet çıkarımı, kontrol senaryosunda 1-2
* sohbet çağrısı; 9 Eki 2026 koşusu: 10 senaryo ≈ 11 çağrı ≈ 0,001 $).
*
* pnpm eval:revizyon # 10 senaryo × 1
* pnpm eval:revizyon --tekrar 2 # her senaryo 2 kez (model stokastik)
* pnpm eval:revizyon --senaryo r02,r04
* pnpm eval:revizyon --zorunlu # ilk adımda tool_choice "required" (route: SOHBET_REVIZYON_ARAC_ZORUNLU=1)
*
* 8 Eki 2026 sonuçları (deepseek-v4-flash @ openrouter): düzeltme öncesi
* liste_revize 1/8, sahte beyan 5 (16 çağrı, 0,0054 $); geçmiş temizliği +
* prompt + --zorunlu ile 7/8, sahte beyan 0 — ama "required" isteği Baidu
* yerine OpenInference'a gitti, gecikme 1-7 sn'den 80-112 sn'ye çıktı (30
* çağrı, 0,0077 $). Zorunlu bayrağı bu yüzden varsayılan kapalı.
* Ne ölçülür (route.ts ile aynı yol): (1) regex kapısı + yapılandırılmış
* niyet çıkarımı (lib/ai/revizyon-niyeti.ts) beklenen dala gidiyor mu —
* revizyon beklenen senaryoda geçerli KURAL, belirsiz istekte FORM, kontrol
* senaryosunda revizyon değil; (2) çıkarılan kural beklenen alanı taşıyor mu
* (ör. ilkTercihler.iller = Bursa); (3) sohbet modeli sunucu işleminin
* sonucunu doğru anlatıyor mu — sahte beyan (liste değişmeden "yenilendi")
* ve sonuçtaki program adlarının cevapta geçmesi; (4) gecikme ve maliyet,
* niyet çağrısı ayrı.
*
* QA bulgusu B1 (docs/ekip-raporlari/2026-10-07/qa-geri-bildirim-testi.md):
* paketli hesapta 7 revizyon isteğinin 4'ünde model aracı çağırmadan "Liste
* yenilendi" yazdı. Kayıtlı geçmişte araç turları görünmediği için başarılı
* bir revizyonun metni de araçsız cevapla aynı görünüyor; model sonraki turda
* bu kalıbı taklit ediyor. Senaryoların geçmişi QA'nın gerçek sohbet
* kaydından (lokal data/app.db, 8 Eki 2026) alınmıştır.
* Tarihçe: 8 Eki 2026'ya kadar revizyon modelin `liste_revize` aracını
* gönüllü çağırmasına bağlıydı; son ölçüm (128c175, 21-19-51 koşusu) 8
* istekte 5 çağrı, 3 sahte beyan; `tool_choice: required` 7/8 verdi ama
* OpenRouter yavaş sağlayıcıya geçti (80-112 sn). Bu yüzden revizyon koda
* bağlandı (QA B1, docs/ekip-raporlari/2026-10-07/qa-geri-bildirim-testi.md).
* Senaryoların geçmişi QA'nın gerçek sohbet kaydından (8 Eki 2026).
*
* Bağlam route.ts gibi kurulur (sistemPromptu + revizyon bölümü +
* kayitliMesajModelMetni + modelPenceresi). `liste_revize` ÇALIŞTIRILMAZ:
* aracBaglami.mesajId verilmediği için araç "bu bağlamda çalıştırılamıyor"
* hatası döner — ölçülen şey çağrının yapılıp yapılmadığıdır, listenin
* yeniden kurulması değil (o, revizyon-denetim.ts'nin işi). Jev denetimi
* kapalı (TYPESAFE_API_KEY silinir); maliyet yalnız sohbet modelidir.
* Liste GERÇEKTEN kurulmaz: koddan işlem sahte bir çekirdekle (sabit 24'lük
* özet, ilk 5 kurala göre havuzdan) taklit edilir; ölçülen, niyetin doğru
* çıkarılması ve modelin sonucu dürüst anlatmasıdır (gerçek üretimi
* revizyon-denetim.ts sınar). Jev kapalı; maliyet yalnız model çağrıları.
*/
import Database from "better-sqlite3";
import { argOku, bayrakVar, evalAppDbHazirla, gitDurumu, kisalt, olcerekKos, sonucYaz, tablo, TOPLAM } from "./ortak";
import type { RaporParams, RaporSonuc } from "@/features/rapor/types/rapor";
import { argOku, evalAppDbHazirla, gitDurumu, kisalt, olcerekKos, sonucYaz, tablo, TOPLAM } from "./ortak";
import type { RaporParams, RaporSonuc, RevizyonKurali } from "@/features/rapor/types/rapor";
import type { KayitliMesaj } from "@/lib/ai/sohbet-gecmisi";
import type { Anket } from "@/lib/ai/sohbet-meta";
import type { KoddanIslem } from "@/lib/ai/sohbet-akisi";
// ── QA kaydından alınan geçmiş parçaları (metinler birebir) ──
const ANKARA_CEVABI = `İlk 5 tercihi Ankara olacak şekilde revize ediyorum.
@@ -105,25 +106,43 @@ type Senaryo = {
aciklama: string;
gecmis: KayitliMesaj[];
soru: string;
/** Hangi araç bekleniyor: revizyon (liste_revize), soru (soru_sor), hicbiri (liste_revize ÇAĞRILMAMALI). */
/** Beklenen dal: revizyon (geçerli kural → koddan revizyon), soru (form), hicbiri (revizyon değil). */
beklenen: "revizyon" | "soru" | "hicbiri";
/** Uygulanmış revizyon kuralı (prompt "UYGULANMIŞ REVİZYON KURALLARI" satırı); QA kaydındaki gibi. */
revizyon?: RaporParams["revizyon"];
/** Çıkarılan kuralda olması beklenen parça (derin alt küme). */
kural?: Partial<RevizyonKurali>;
/** Form cevabı senaryosu: cevaplanan form (route sonAnket'i verir). */
oncekiForm?: Anket;
};
const SON_KURAL: RaporParams["revizyon"] = { ilkTercihler: { adet: 5, iller: ["İZMİR"] }, iller: { ekle: ["ANKARA"] }, not: "İlk 5 tercih İzmir olsun." };
const SENARYOLAR: Senaryo[] = [
{ id: "r01", aciklama: "geçmiş yok, net istek", gecmis: [], soru: "İlk 5'i İstanbul'a göre güncelle.", beklenen: "revizyon" },
{ id: "r02", aciklama: "1 başarılı revizyon geçmişte", gecmis: G1, soru: "İlk 5'i İzmir'e göre güncelle.", beklenen: "revizyon", revizyon: { ilkTercihler: { adet: 5, iller: ["ANKARA"] } } },
{ id: "r03", aciklama: "2 başarılı revizyon geçmişte (QA 3. istek)", gecmis: G2, soru: "İlk 5'i Bursa'ya göre güncelle.", beklenen: "revizyon", revizyon: SON_KURAL },
{ id: "r04", aciklama: "form cevabı (QA'da araçsız kaldı)", gecmis: G_FORM, soru: FORM_CEVABI, beklenen: "revizyon", revizyon: SON_KURAL },
{ id: "r05", aciklama: "geçmişte sahte beyan + düzeltme notu", gecmis: G_SAHTE, soru: "Listeyi güncelle: ilk 3 tercih İzmir'de olsun.", beklenen: "revizyon", revizyon: SON_KURAL },
{ id: "r06", aciklama: "2 geçmiş, tip kuralı", gecmis: G2, soru: "vakıf olmasın, sadece devlet", beklenen: "revizyon", revizyon: SON_KURAL },
{ id: "r07", aciklama: "2 geçmiş, iskelet kuralı", gecmis: G2, soru: "listem daha güvenli olsun", beklenen: "revizyon", revizyon: SON_KURAL },
{ id: "r08", aciklama: "2 geçmiş, il ekleme", gecmis: G2, soru: "Kocaeli de olsun", beklenen: "revizyon", revizyon: SON_KURAL },
{ id: "r01", aciklama: "geçmiş yok, net istek", gecmis: [], soru: "İlk 5'i İstanbul'a göre güncelle.", beklenen: "revizyon", kural: { ilkTercihler: { adet: 5, iller: ["İSTANBUL"] } } },
{ id: "r02", aciklama: "1 başarılı revizyon geçmişte", gecmis: G1, soru: "İlk 5'i İzmir'e göre güncelle.", beklenen: "revizyon", revizyon: { ilkTercihler: { adet: 5, iller: ["ANKARA"] } }, kural: { ilkTercihler: { adet: 5, iller: ["İZMİR"] } } },
{ id: "r03", aciklama: "2 başarılı revizyon geçmişte (QA 3. istek)", gecmis: G2, soru: "İlk 5'i Bursa'ya göre güncelle.", beklenen: "revizyon", revizyon: SON_KURAL, kural: { ilkTercihler: { adet: 5, iller: ["BURSA"] } } },
{ id: "r04", aciklama: "form cevabı (QA'da araçsız kaldı)", gecmis: G_FORM, soru: FORM_CEVABI, beklenen: "revizyon", revizyon: SON_KURAL, oncekiForm: ANKET, kural: { iskelet: "hayal-agirlikli", universiteTipi: "devlet", iller: { ekle: ["İSTANBUL"] } } },
{ id: "r05", aciklama: "geçmişte sahte beyan + düzeltme notu", gecmis: G_SAHTE, soru: "Listeyi güncelle: ilk 3 tercih İzmir'de olsun.", beklenen: "revizyon", revizyon: SON_KURAL, kural: { ilkTercihler: { adet: 3, iller: ["İZMİR"] } } },
{ id: "r06", aciklama: "2 geçmiş, tip kuralı", gecmis: G2, soru: "vakıf olmasın, sadece devlet", beklenen: "revizyon", revizyon: SON_KURAL, kural: { universiteTipi: "devlet" } },
{ id: "r07", aciklama: "2 geçmiş, iskelet kuralı", gecmis: G2, soru: "listem daha güvenli olsun", beklenen: "revizyon", revizyon: SON_KURAL, kural: { iskelet: "guvenli-agirlikli" } },
{ id: "r08", aciklama: "2 geçmiş, il ekleme", gecmis: G2, soru: "Kocaeli de olsun", beklenen: "revizyon", revizyon: SON_KURAL, kural: { iller: { ekle: ["KOCAELİ"] } } },
{ id: "r09", aciklama: "2 geçmiş, belirsiz istek → form", gecmis: G2, soru: "listemi biraz değiştirmek istiyorum", beklenen: "soru", revizyon: SON_KURAL },
{ id: "r10", aciklama: "2 geçmiş, revizyon değil (kontrol)", gecmis: G2, soru: "listemdeki 1. programın kontenjanı kaç, geçen yıl kaç kişi yerleşmiş?", beklenen: "hicbiri", revizyon: SON_KURAL },
];
/** `beklenen` kural parçası `gercek`in alt kümesi mi (diziler sırasız, büyük-küçük harf duyarsız). */
function altKume(beklenen: unknown, gercek: unknown): boolean {
if (Array.isArray(beklenen)) {
if (!Array.isArray(gercek)) return false;
const g = gercek.map((x) => String(x).toLocaleLowerCase("tr-TR"));
return beklenen.every((b) => g.includes(String(b).toLocaleLowerCase("tr-TR")));
}
if (beklenen && typeof beklenen === "object") {
if (!gercek || typeof gercek !== "object") return false;
return Object.entries(beklenen).every(([k, v]) => altKume(v, (gercek as Record<string, unknown>)[k]));
}
return beklenen === gercek;
}
const PARAMS: RaporParams = { sira: 85000, tur: "say", kategoriler: ["Mühendislik & Teknoloji"], iller: ["ANKARA", "İZMİR", "BURSA"], universiteTipi: "farketmez", oncelikler: [], ozelDurum: [] };
async function main() {
@@ -133,18 +152,18 @@ async function main() {
const { danismanAkisi } = await import("@/lib/ai/sohbet-akisi");
const { sistemPromptu } = await import("@/lib/ai/danisman-prompt");
const { kayitliMesajModelMetni, modelPenceresi, revizyonNiyetiVar } = await import("@/lib/ai/sohbet-gecmisi");
const { barajTablosu } = await import("@/lib/ai/araclar");
const { barajTablosu, listeRevizeSonucuBicimle, soruFormuSonucu } = await import("@/lib/ai/araclar");
const { revizyonNiyetiCikar } = await import("@/lib/ai/revizyon-niyeti");
const { listeOzetiCikar } = await import("@/lib/ai/rapor");
const { sohbetModeli, saglayiciAdi, anahtarVar } = await import("@/lib/ai/client");
const { sohbetModeli, niyetModeli, saglayiciAdi, anahtarVar } = await import("@/lib/ai/client");
const { havuzOlustur, dilimHedefiHesapla, idealHedef } = await import("@/lib/rapor-havuzu");
const { REVIZE_DUZELTME_NOTU } = await import("@/lib/ai/cikti-denetimi");
const { kuralOzeti } = await import("@/lib/revizyon-kurali");
if (!anahtarVar()) {
console.error("Model anahtarı yok (AI_API_KEY / OPENROUTER_API_KEY): gerçek modelle koşulamadı.");
process.exit(2);
}
const secilenler = argOku("senaryo")?.split(",").filter(Boolean);
const tekrar = Math.max(1, Math.floor(Number(argOku("tekrar")) || 1));
const zorunlu = bayrakVar("zorunlu");
const senaryolar = SENARYOLAR.filter((s) => !secilenler || secilenler.includes(s.id));
// Liste bağlamı deterministik (model çağrısı yok): her dilimden tabanı en iyi `hedef` kadar program
@@ -159,8 +178,18 @@ async function main() {
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;
const listeOzeti = listeOzetiCikar(rapor, PARAMS.sira, { kimlik: true, gerekce: true });
const tr = (x: string) => x.toLocaleLowerCase("tr-TR");
/** Sahte çekirdek: ilk 5, kuralın ilkTercihler.iller'ine uyan havuz programlarından; yoksa listenin başı. */
const sahteOzet = (kural: RevizyonKurali) => {
const iller = (kural.ilkTercihler?.iller ?? []).map(tr);
const adet = Math.min(5, kural.ilkTercihler?.adet ?? 5);
const uyan = iller.length ? havuz.filter((x) => iller.includes(tr(x.il ?? ""))).slice(0, adet) : [];
const kalan = secilen.filter((x) => !uyan.includes(x)).slice(0, 5 - uyan.length);
const dagilim = kural.iskelet === "guvenli-agirlikli" ? { hayal: 4, dengeli: 12, garanti: 8 } : kural.iskelet === "hayal-agirlikli" ? { hayal: 7, dengeli: 12, garanti: 5 } : { hayal: 5, dengeli: 13, garanti: 6 };
return { ok: true as const, toplam: 24, dagilim, ilkTercihler: [...uyan, ...kalan].map((x) => `${x.isim} — ${x.universite} (${x.il})`) };
};
console.log(`\n== Revizyon aracı eval — ${senaryolar.length} senaryo${tekrar > 1 ? ` × ${tekrar}` : ""}, model ${sohbetModeli()} @ ${saglayiciAdi()}, ilk adım araç ${zorunlu ? "ZORUNLU" : "serbest"} ==\n`);
console.log(`\n== Koda bağlı revizyon eval — ${senaryolar.length} senaryo${tekrar > 1 ? ` × ${tekrar}` : ""}, sohbet ${sohbetModeli()}, niyet ${niyetModeli()} @ ${saglayiciAdi()} ==\n`);
const asilInfo = console.info;
console.info = () => {};
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, ?, ?)");
@@ -173,20 +202,51 @@ async function main() {
const mesajlar = modelPenceresi(s.gecmis.map(kayitliMesajModelMetni), s.soru);
const userId = `eval-rev-${process.pid}-${s.id}-${t}`;
kullaniciEkle.run(userId, `${userId}@eval.invalid`, Date.now(), Date.now());
// 1. Niyet: regex kapısı + yapılandırılmış çıkarım (route.ts ile aynı)
const regex = revizyonNiyetiVar(s.soru);
let dal: "revizyon" | "form" | "sohbet" = "sohbet";
let kural: RevizyonKurali | null = null;
let eksik: string[] = [];
let niyetAdi = "—";
let niyetSaglayici: string | null = null;
let islem: KoddanIslem | undefined;
let ozet: ReturnType<typeof sahteOzet> | null = null;
const t0 = Date.now();
const niyetOlcum = regex
? await olcerekKos(() => revizyonNiyetiCikar({ mesaj: s.soru, params, oncekiForm: s.oncekiForm ?? null, gozlem: (g) => (niyetSaglayici = g.saglayici) }))
: null;
const niyetMs = Date.now() - t0;
if (niyetOlcum) {
const n = niyetOlcum.sonuc;
niyetAdi = n.niyet;
if (n.niyet === "revizyon" && "kural" in n) {
dal = "revizyon";
kural = n.kural;
ozet = sahteOzet(n.kural);
const sonuc = listeRevizeSonucuBicimle({ ok: true, ozet, rapor });
islem = { ad: "liste_revize", girdi: n.kural, calistir: async () => sonuc, anlatim: { ozet: kuralOzeti(n.kural), ilkN: Boolean(n.kural.ilkTercihler) } };
} else if (n.niyet === "revizyon") {
dal = "form";
eksik = n.eksik;
const sonuc = soruFormuSonucu(n.form);
islem = { ad: "soru_sor", girdi: {}, calistir: async () => sonuc };
} else if (n.niyet === "belirsiz") niyetAdi = `belirsiz (${n.neden})`;
}
// 2. Sohbet: koddan işlem + model anlatımı
const araclar: string[] = [];
let metin = "";
let revizeNotu = false;
let hata: string | null = null;
const t0 = Date.now();
const t1 = Date.now();
const { kullanim } = await olcerekKos(async () => {
try {
const okuyucu = danismanAkisi({
system,
mesajlar,
// mesajId YOK: liste_revize çalıştırılmaz, "bu bağlamda çalıştırılamıyor" döner
aracBaglami: { userId, sira: PARAMS.sira, tur: PARAMS.tur, hasPaket: true, profil: {}, oncelikler: [], ozelDurum: [], sihirbazIlVar: true },
// route.ts ile aynı: --zorunlu (= SOHBET_REVIZYON_ARAC_ZORUNLU=1) + revizyon niyeti → ilk adım araç zorunlu
ilkAdimAracZorunlu: zorunlu && revizyonNiyetiVar(s.soru),
islem,
onTamam: async (m) => {
metin = m;
},
@@ -197,6 +257,7 @@ async function main() {
onDenetim: (d) => {
revizeNotu = d.revizeNotu;
},
onRevizeBeyani: async () => true,
}).getReader();
for (;;) {
const { done, value } = await okuyucu.read();
@@ -209,35 +270,43 @@ async function main() {
hata = e instanceof Error ? e.message : String(e);
}
});
const ms = Date.now() - t0;
const revize = araclar.includes("liste_revize");
const soru = araclar.includes("soru_sor");
const tuttu = s.beklenen === "revizyon" ? revize : s.beklenen === "soru" ? soru || revize : !revize;
const sahteBeyan = !revize && (revizeNotu || metin.includes(REVIZE_DUZELTME_NOTU));
sonuclar.push({ id: s.id, tekrar: t, aciklama: s.aciklama, beklenen: s.beklenen, aracZorunlu: zorunlu && revizyonNiyetiVar(s.soru), araclar, tuttu, sahteBeyan, hata, ms, modelCagrisi: kullanim.cagri, maliyet: kullanim.maliyet, cevap: metin.replace(REVIZE_DUZELTME_NOTU, "").trim() });
asilInfo(` ${tuttu ? "geçti" : "KALDI"} ${s.id}${tekrar > 1 ? `#${t + 1}` : ""} [${s.beklenen}] ${(ms / 1000).toFixed(1)} sn, ${kullanim.cagri} çağrı, araç: ${araclar.join("+") || "—"}${sahteBeyan ? " ⇒ SAHTE BEYAN (araçsız 'yenilendi')" : ""}${hata ? ` ⇒ hata: ${kisalt(hata, 60)}` : ""}\n ${kisalt(metin.replace(/\s+/g, " "), 160)}`);
const sohbetMs = Date.now() - t1;
const kuralTuttu = !s.kural || (kural != null && altKume(s.kural, kural));
const tuttu = s.beklenen === "revizyon" ? dal === "revizyon" && kuralTuttu : s.beklenen === "soru" ? dal === "form" : dal !== "revizyon";
const sahteBeyan = dal !== "revizyon" && revizeNotu;
// Anlatım: sonuçtaki ilk tercihlerin bölüm adları cevapta geçiyor mu (uydurma ad yerine sunucu sonucu)
const m = tr(metin);
const adEslesme = ozet ? ozet.ilkTercihler.filter((x) => m.includes(tr(x.split(" — ")[0]))).length : null;
const niyetMaliyet = niyetOlcum?.kullanim.maliyet ?? 0;
sonuclar.push({ id: s.id, tekrar: t, aciklama: s.aciklama, beklenen: s.beklenen, regex, niyet: niyetAdi, dal, kural, eksik, kuralTuttu, tuttu, sahteBeyan, adEslesme, araclar, hata, niyetMs, sohbetMs, ms: niyetMs + sohbetMs, niyetSaglayici, niyetCagri: niyetOlcum?.kullanim.cagri ?? 0, niyetMaliyet, sohbetCagri: kullanim.cagri, sohbetMaliyet: kullanim.maliyet, cevap: metin.trim() });
asilInfo(` ${tuttu ? "geçti" : "KALDI"} ${s.id}${tekrar > 1 ? `#${t + 1}` : ""} [${s.beklenen}] niyet ${niyetAdi} → ${dal}${kural ? ` ${kisalt(JSON.stringify(kural), 90)}` : ""}${!kuralTuttu ? " ⇒ KURAL BEKLENENDEN FARKLI" : ""} · niyet ${(niyetMs / 1000).toFixed(1)} sn + sohbet ${(sohbetMs / 1000).toFixed(1)} sn, ${(niyetOlcum?.kullanim.cagri ?? 0) + kullanim.cagri} çağrı, araç: ${araclar.join("+") || "—"}${adEslesme != null ? `, ad eşleşme ${adEslesme}/5` : ""}${sahteBeyan ? " ⇒ SAHTE BEYAN" : ""}${hata ? ` ⇒ hata: ${kisalt(hata, 60)}` : ""}\n ${kisalt(metin.replace(/\s+/g, " "), 160)}`);
}
}
console.info = asilInfo;
const rev = sonuclar.filter((x) => x.beklenen === "revizyon");
const revCagri = rev.filter((x) => (x.araclar as string[]).includes("liste_revize")).length;
const kuruldu = rev.filter((x) => x.dal === "revizyon").length;
const kuralDogru = rev.filter((x) => x.dal === "revizyon" && x.kuralTuttu).length;
const sahte = sonuclar.filter((x) => x.sahteBeyan).length;
const niyetSureleri = sonuclar.filter((x) => x.regex).map((x) => x.niyetMs as number);
const ozet = {
model: sohbetModeli(),
zorunlu,
sohbetModeli: sohbetModeli(),
niyetModeli: niyetModeli(),
saglayici: saglayiciAdi(),
n: sonuclar.length,
gecti: sonuclar.filter((x) => x.tuttu).length,
revizyonBeklenen: rev.length,
listeRevizeCagrildi: revCagri,
listeRevizeOrani: rev.length ? Math.round((100 * revCagri) / rev.length) : 0,
koddanRevizyon: kuruldu,
kuralDogru,
sahteBeyan: sahte,
niyetCagri: sonuclar.reduce((a, x) => a + (x.niyetCagri as number), 0),
niyetMaliyet: sonuclar.reduce((a, x) => a + (x.niyetMaliyet as number), 0),
niyetMsOrt: niyetSureleri.length ? Math.round(niyetSureleri.reduce((a, b) => a + b, 0) / niyetSureleri.length) : null,
harcama: { ...TOPLAM },
};
console.log(`\n${tablo(["senaryo", "beklenen", "araç", "sonuç", "sahte beyan"], sonuclar.map((x) => [String(x.id), String(x.beklenen), (x.araclar as string[]).join("+") || "—", x.tuttu ? "geçti" : "KALDI", x.sahteBeyan ? "EVET" : "-"]))}`);
console.log(`\nliste_revize çağrı oranı (revizyon beklenen): ${revCagri}/${rev.length} (%${ozet.listeRevizeOrani}) · sahte beyan: ${sahte} · geçen: ${ozet.gecti}/${ozet.n}`);
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)` : ""}`);
const yol = sonucYaz("revizyon", { tur: "revizyon-arac", tarih: new Date().toISOString(), git: gitDurumu(), argv: process.argv.slice(2), ozet, senaryolar: sonuclar });
console.log(`\n${tablo(["senaryo", "beklenen", "niyet", "dal", "kural", "sonuç", "sahte", "niyet sn", "sohbet sn"], sonuclar.map((x) => [String(x.id), String(x.beklenen), String(x.niyet), String(x.dal), x.kuralTuttu ? "ok" : "FARKLI", x.tuttu ? "geçti" : "KALDI", x.sahteBeyan ? "EVET" : "-", ((x.niyetMs as number) / 1000).toFixed(1), ((x.sohbetMs as number) / 1000).toFixed(1)]))}`);
console.log(`\nkoddan revizyon (revizyon beklenen): ${kuruldu}/${rev.length}, kural beklenen alanı taşıyor: ${kuralDogru}/${rev.length} · sahte beyan: ${sahte} · geçen: ${ozet.gecti}/${ozet.n} · niyet çağrısı ort. ${ozet.niyetMsOrt ?? "—"} ms`);
console.log(`Harcama: ${TOPLAM.cagri} model çağrısı (${ozet.niyetCagri} niyet), ${TOPLAM.girdiToken} girdi + ${TOPLAM.ciktiToken} çıktı token, ${TOPLAM.maliyet.toFixed(4)} $ (niyet ${ozet.niyetMaliyet.toFixed(4)} $)${TOPLAM.maliyetBilinmeyenCagri ? ` (+${TOPLAM.maliyetBilinmeyenCagri} çağrının maliyeti bildirilmedi)` : ""}`);
const yol = sonucYaz("revizyon", { tur: "revizyon-koda-bagli", tarih: new Date().toISOString(), git: gitDurumu(), argv: process.argv.slice(2), ozet, senaryolar: sonuclar });
console.log(`Ham sonuç: ${yol}`);
sqlite.close();
process.exit(0);