Files
kolaytercih/scripts/eval/akis-denetim.ts
bilalgursen e9dfa354f8 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>
2026-10-09 02:10:59 +03:00

223 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Danışman akışındaki sunucu denetimlerinin MEKANİK testi — parasız, anahtarsız.
*
* pnpm tsx --conditions=react-server scripts/eval/akis-denetim.ts
*
* Yerelde sahte bir OpenAI-uyumlu uç açılır; "model" senaryodaki metni ya da
* araç çağrılarını aynen döner. Gerçek danismanAkisi + gerçek araçlar koşar
* (profil_guncelle data/app.db'nin geçici kopyasına yazar). Doğrulananlar:
* vaat regex'i notu (Jev anahtarı yokken), "kaydettim" deyip kaydetmeme notu,
* araç çağrısı kotası, rehber bağlantısı doğrulama (uydurma slug akışta
* gerçek slug'a döner, kayıt da düzeltilmiş metni alır), koda bağlı revizyon
* (9 Eki 2026: koddan işlem ekrana tool-liste_revize parçası ve modele
* "sunucu işlemi" bölümü olarak gider, "yeniden kurdum" o zaman beyan
* sayılmaz; işlem yokken beyan → not + kredi iadesi; koddan form → model
* araçsız; liste_revize modelin araç listesinde yok). Model davranışını
* ÖLÇMEZ; onu eval:sohbet ve eval:revizyon ölçer.
* Beklenti tutmazsa çıkış kodu 1.
*/
import { createServer } from "node:http";
import { copyFileSync } from "node:fs";
import { execFileSync } from "node:child_process";
type Beklenen = { vaatNotu: boolean; kayitNotu: boolean; reddedilenArac: number; baglantiDuzeltme: number; revizeNotu?: boolean };
type Senaryo = {
ad: string;
beklenen: Beklenen;
adimlar: ({ metin: string } | { araclar: { name: string; arguments: string }[] })[];
/** Kaydedilen metinde olması / olmaması gerekenler (rehber bağlantısı senaryoları). */
metinde?: { var?: string[]; yok?: string[] };
/** Koddan işlem (route'un revizyon-niyeti sonucuna göre verdiği). */
islem?: { ad: "liste_revize" | "soru_sor"; cikti: unknown; anlatim?: { ozet: string[]; ilkN: boolean } };
/** Sahte uca giden İLK isteğin denetimi: sistem metninde olan, araç listesi, tool_choice. */
istek?: { sistemde?: string[]; sistemdeYok?: string[]; aracYok?: string[]; toolChoice?: string };
/** Ekrana giden araç parçaları (tool-input-available toolName:durum). */
parcalar?: string[];
/** onRevizeBeyani çağrıldı mı (kredi iadesi yolu). */
iadeIstendi?: boolean;
/** Sahte uca giden istek sayısı (koddan cevapta 0). */
modelCagrisi?: number;
};
const B = (vaatNotu: boolean, kayitNotu: boolean, reddedilenArac = 0, baglantiDuzeltme = 0, revizeNotu = false): Beklenen => ({ vaatNotu, kayitNotu, reddedilenArac, baglantiDuzeltme, revizeNotu });
const REVIZYON_OK = { ok: true, toplam: 24, dagilim: { hayal: 5, dengeli: 13, garanti: 6 }, ilkTercihler: ["Bilgisayar Mühendisliği — EGE ÜNİVERSİTESİ (İZMİR)"], not: "Liste ekranda yenilendi." };
const FORM = { soruldu: true, anket: { id: "f1", sorular: [{ ad: "il", soru: "Hangi il?", secenekler: [{ deger: "izmir", etiket: "İzmir" }, { deger: "ankara", etiket: "Ankara" }] }] }, not: "Form adaya gösterildi." };
const SENARYOLAR: Senaryo[] = [
{ ad: "kaydettim-aracsiz", beklenen: B(false, true), adimlar: [{ metin: "Kaydettim: Vakıfta yıllık ücret üst sınırı 400.000 TL. Aramalar buna göre süzülecek." }] },
{ ad: "kaydettim-aracli", beklenen: B(false, false), adimlar: [{ araclar: [{ name: "profil_guncelle", arguments: JSON.stringify({ alan: "ucretUst", deger: 400000 }) }] }, { metin: "Kaydettim: yıllık ücret en fazla 400.000 TL." }] },
{ ad: "kaydettim-arac-hata", beklenen: B(false, true), adimlar: [{ araclar: [{ name: "profil_guncelle", arguments: JSON.stringify({ alan: "maxKm", deger: 30 }) }] }, { metin: "Kaydettim: evden en fazla 30 km." }] },
{ ad: "kaydetmedim-not-ettim", beklenen: B(false, false), adimlar: [{ metin: "Tamam, kaydetmedim. İzmir'de oturduğunu not ettim; listen zaten İzmir ağırlıklı." }] },
{ ad: "vaat", beklenen: B(true, false), adimlar: [{ metin: "Bu programa yerleşme olasılığın düşük ama yine de yazabilirsin." }] },
{ ad: "temiz", beklenen: B(false, false), adimlar: [{ metin: "Bu program son yerleştirmede 178.278. sırada kapandı; etiketi Riskli. Garanti verilemez." }] },
{ ad: "kota", beklenen: B(false, false, 2), adimlar: [{ araclar: Array.from({ length: 6 }, (_, i) => ({ name: "il_ozeti", arguments: JSON.stringify({ il: ["İzmir", "Ankara", "Bursa", "Konya", "Adana", "Samsun"][i] }) })) }, { metin: "İllere baktım." }] },
// Model araç çağırmadan ezberden iki yanlış rehber adresi yazar; ikisi de akışta gerçek slug'a döner
{
ad: "rehber-uydurma",
beklenen: B(false, false, 0, 2),
adimlar: [{ metin: "Şu yazılara bak: [OBP](/rehber/obp-nedir) ve [Yatay geçiş](/rehber/yatay-gecis-rehberi). İkisi de sana göre." }],
metinde: { var: ["/rehber/obp-nedir-nasil-hesaplanir", "/rehber/yatay-gecis-cap-yandal"], yok: ["/rehber/obp-nedir)", "/rehber/yatay-gecis-rehberi"] },
},
{
ad: "rehber-gercek",
beklenen: B(false, false, 0, 0),
adimlar: [{ metin: "Bak: [Devlet mi vakıf mı](/rehber/devlet-mi-vakif-mi) yazısı bunu anlatıyor." }],
metinde: { var: ["/rehber/devlet-mi-vakif-mi"] },
},
// Koda bağlı revizyon (QA B1): niyet var + kural geçerli → işlem ve cevap
// koddan (model çağrılmaz); ekranda tool-liste_revize parçası; sayılar ve
// adlar sunucu sonucundan şablonla.
{
ad: "koddan-revizyon",
beklenen: B(false, false),
islem: { ad: "liste_revize", cikti: REVIZYON_OK, anlatim: { ozet: ["ilk 5 tercih: il İZMİR"], ilkN: true } },
modelCagrisi: 0,
parcalar: ["liste_revize:output-available"],
iadeIstendi: false,
adimlar: [{ metin: "(model çağrılmamalı)" }],
metinde: { var: ["Listeni yeniden kurdum — ilk 5 tercih: il İzmir. 24 tercih: 5 hayal / 13 dengeli / 6 güvenli.", "1. Bilgisayar Mühendisliği — Ege Üniversitesi (İzmir)", "dilim sırası bilerek bozulur"], yok: ["YENİDEN KURULMADI", "model çağrılmamalı"] },
},
// Niyet regex'i eşleşti ama çıkarım "diger" dedi → işlem yok, normal sohbet;
// model yine de "yeniden kurdum" derse not + kredi iadesi (güvenlik ağı).
{
ad: "niyet-diger-sahte-beyan",
beklenen: B(false, false, 0, 0, true),
istek: { sistemdeYok: ["BU TURDA SUNUCU İŞLEMİ"], aracYok: ["liste_revize"] },
parcalar: [],
iadeIstendi: true,
adimlar: [{ metin: "Listeni yeniden kurdum, artık ilk 5 tercihin İzmir'den." }],
metinde: { var: ["YENİDEN KURULMADI", "kredisi iade edildi"] },
},
// Niyet var, kural eksik → form koddan; cevap da koddan (model çağrısı yok)
{
ad: "koddan-form",
beklenen: B(false, false),
islem: { ad: "soru_sor", cikti: FORM },
modelCagrisi: 0,
parcalar: ["soru_sor:output-available"],
iadeIstendi: false,
adimlar: [{ metin: "(model çağrılmamalı)" }],
metinde: { var: ["Birkaç kısa soru hazırladım"], yok: ["model çağrılmamalı"] },
},
// Koddan revizyon başarısız: ekrana hata parçası, cevap koddan nedeniyle (model yok)
{
ad: "koddan-revizyon-hata",
beklenen: B(false, false),
islem: { ad: "liste_revize", cikti: { hata: "Liste şu anda zaten yeniden kuruluyor.", not: "'yeniden kurdum' deme." } },
modelCagrisi: 0,
parcalar: ["liste_revize:output-error"],
iadeIstendi: false,
adimlar: [{ metin: "(model çağrılmamalı)" }],
metinde: { var: ["Listeni yeniden kuramadım: Liste şu anda zaten yeniden kuruluyor."], yok: ["model çağrılmamalı"] },
},
];
const istekler: { system: string; araclar: string[]; toolChoice: unknown }[] = [];
let aktif: Senaryo = SENARYOLAR[0];
let adimNo = 0;
const sunucu = createServer((req, res) => {
let govde = "";
req.on("data", (c) => (govde += c));
req.on("end", () => {
try {
const j = JSON.parse(govde) as { messages?: { role: string; content: string }[]; tools?: { function?: { name: string } }[]; tool_choice?: unknown };
istekler.push({ system: j.messages?.find((m) => m.role === "system")?.content ?? "", araclar: (j.tools ?? []).map((t) => t.function?.name ?? "?"), toolChoice: j.tool_choice ?? null });
} catch {}
const adim = aktif.adimlar[Math.min(adimNo++, aktif.adimlar.length - 1)];
res.writeHead(200, { "Content-Type": "text/event-stream" });
const yaz = (delta: unknown, finish: string | null = null) =>
res.write(`data: ${JSON.stringify({ id: "x", object: "chat.completion.chunk", created: 1, model: "sahte", choices: [{ index: 0, delta, finish_reason: finish }] })}\n\n`);
if ("metin" in adim) {
yaz({ role: "assistant", content: adim.metin });
yaz({}, "stop");
} else {
yaz({ role: "assistant", content: null, tool_calls: adim.araclar.map((a, i) => ({ index: i, id: `c${adimNo}_${i}`, type: "function", function: a })) });
yaz({}, "tool_calls");
}
res.write("data: [DONE]\n\n");
res.end();
});
});
async function main() {
await new Promise<void>((r) => sunucu.listen(0, "127.0.0.1", r));
const port = (sunucu.address() as { port: number }).port;
process.env.AI_BASE_URL = `http://127.0.0.1:${port}/v1`;
process.env.AI_API_KEY = "sahte-anahtar";
delete process.env.TYPESAFE_API_KEY; // Jev'e gitme: yalnız regex yolu
const db = `${process.env.TMPDIR ?? "/tmp"}/kolaytercih-akis-dene-${process.pid}.db`;
copyFileSync("data/app.db", db);
process.env.APP_DB_URL = `file:${db}`;
execFileSync("node", ["scripts/db-goc.mjs"], { env: { ...process.env }, stdio: "ignore" });
const Database = (await import("better-sqlite3")).default;
const sqlite = new Database(db);
const { danismanAkisi } = await import("@/lib/ai/sohbet-akisi");
// Gerçek content/rehber listesi: bağlantı doğrulama tamponu aktif
const { getRehberAramaKayitlari } = await import("@/lib/rehber");
const rehberler = getRehberAramaKayitlari();
const info = console.info;
console.info = () => {};
let hata = 0;
for (const s of SENARYOLAR) {
aktif = s;
adimNo = 0;
const userId = `akis-dene-${s.ad}`;
sqlite.prepare("INSERT OR REPLACE INTO user (id, name, email, email_verified, credit_balance, has_paket, created_at, updated_at) VALUES (?, 'Dene', ?, 1, 0, 1, 1, 1)").run(userId, `${userId}@eval.invalid`);
let tam: string | null = null;
let denetim: Beklenen | null = null;
let iadeIstendi = false;
const araclar: string[] = [];
const parcalar: string[] = [];
const parcaAdi = new Map<string, string>();
istekler.length = 0;
const okuyucu = danismanAkisi({
system: "test",
mesajlar: [{ role: "user", content: "test" }],
aracBaglami: { userId, sira: 100000, tur: "say", profil: {} },
rehberler,
islem: s.islem ? { ad: s.islem.ad, girdi: {}, calistir: async () => s.islem!.cikti, anlatim: s.islem.anlatim } : undefined,
onRevizeBeyani: async () => {
iadeIstendi = true;
return true;
},
onTamam: async (m) => {
tam = m;
},
onYarim: async () => {},
onDenetim: (d) => {
denetim = d;
},
}).getReader();
for (;;) {
const { done, value } = await okuyucu.read();
if (done) break;
const v = value as { type: string; output?: unknown; toolCallId?: string; toolName?: string };
if (v.type === "tool-input-available" && v.toolCallId) parcaAdi.set(v.toolCallId, v.toolName ?? "?");
if (v.type === "tool-output-available") araclar.push(JSON.stringify(v.output).slice(0, 70));
if ((v.type === "tool-output-available" || v.type === "tool-output-error") && v.toolCallId?.startsWith("kod-")) parcalar.push(`${parcaAdi.get(v.toolCallId)}:${v.type.slice("tool-".length)}`);
}
const d = denetim as Beklenen | null;
const kayit = String(tam);
const metinTuttu = (s.metinde?.var ?? []).every((m) => kayit.includes(m)) && (s.metinde?.yok ?? []).every((m) => !kayit.includes(m));
const ilk = istekler[0];
const istekTuttu =
!s.istek ||
(ilk != null &&
(s.istek.sistemde ?? []).every((m) => ilk.system.includes(m)) &&
(s.istek.sistemdeYok ?? []).every((m) => !ilk.system.includes(m)) &&
(s.istek.aracYok ?? []).every((a) => !ilk.araclar.includes(a)) &&
(s.istek.toolChoice === undefined || ilk.toolChoice === s.istek.toolChoice));
const parcaTuttu = !s.parcalar || JSON.stringify(parcalar) === JSON.stringify(s.parcalar);
const iadeTuttu = s.iadeIstendi === undefined || iadeIstendi === s.iadeIstendi;
const cagriTuttu = s.modelCagrisi === undefined || istekler.length === s.modelCagrisi;
const tuttu = d != null && d.vaatNotu === s.beklenen.vaatNotu && d.kayitNotu === s.beklenen.kayitNotu && d.reddedilenArac === s.beklenen.reddedilenArac && d.baglantiDuzeltme === s.beklenen.baglantiDuzeltme && d.revizeNotu === s.beklenen.revizeNotu && metinTuttu && istekTuttu && parcaTuttu && iadeTuttu && cagriTuttu;
if (!tuttu) hata++;
info(`\n${tuttu ? "geçti" : "KALDI"} [${s.ad}]\n denetim: ${JSON.stringify(denetim)}${s.istek ? `\n istek: ${istekTuttu ? "ok" : "BEKLENENDEN FARKLI"} (araçlar ${ilk?.araclar.length ?? "?"}, tool_choice ${JSON.stringify(ilk?.toolChoice)})` : ""}${s.parcalar ? `\n koddan parçalar: ${parcalar.join(", ") || "—"}${parcaTuttu ? "" : " (BEKLENENDEN FARKLI)"}` : ""}${s.iadeIstendi !== undefined ? `\n iade istendi: ${iadeIstendi}` : ""}${s.modelCagrisi !== undefined ? `\n model çağrısı: ${istekler.length}` : ""}\n araç çıktıları: ${araclar.length} → ${araclar.join(" | ")}\n son metin: ${String(tam).replace(/\n+/g, " ⏎ ")}`);
}
sqlite.close();
sunucu.close();
info(`\n${SENARYOLAR.length - hata}/${SENARYOLAR.length} senaryo geçti (gerçek modelle koşulmadı; mekanik test)`);
process.exit(hata ? 1 : 0);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});