/** * Revizyonun "ilk N" garantisinin MEKANİK testi — parasız, anahtarsız. * * pnpm tsx --conditions=react-server scripts/eval/revizyon-denetim.ts * * Yerelde sahte bir OpenAI-uyumlu uç açılır; "model" isteğin json_schema * adetlerini ve kullanıcı mesajındaki ADAY HAVUZU'nu okuyup her listeyi * KASTEN `oncelikli` olmayan id'lerle tam adet doldurur (7 Eki 2026 kök * nedeni: model öncelikliyi atlayınca onarım tamamlayacak boşluk bulamıyor, * liste değişmemiş görünüyordu). Gerçek raporUretAyrintili + gerçek havuz * (data/yokatlas.db, salt okunur) koşar; app.db gerekmez. Doğrulanan: ölçüte * uyan satırların kodla takas edilip başa alınması, dilim adetlerinin * korunması, havuz yetersizken hata fırlamaması ve dev mock yolunun da kuralı * uygulaması. Model davranışını ÖLÇMEZ; onu eval:sohbet ölçer. Beklenti * tutmazsa çıkış kodu 1. */ import { createServer } from "node:http"; import type { RaporParams } from "@/features/rapor/types/rapor"; const tr = (s: string | null | undefined) => (s ?? "").toLocaleLowerCase("tr-TR"); const HAVUZ_ISARETI = "ADAY HAVUZU (yalnızca buradan seç; dizi adı = liste adı): "; type TelListe = "hayal" | "dengeli" | "guvenli"; const LISTELER: TelListe[] = ["hayal", "dengeli", "guvenli"]; /** Sahte ucun son isteğinde kasten atlanan öncelikli satır sayısı (bilgi amaçlı). */ let atlananOncelikli = 0; const sunucu = createServer((req, res) => { let govde = ""; req.on("data", (c) => (govde += c)); req.on("end", () => { const istek = JSON.parse(govde) as { messages: { role: string; content: string }[]; response_format: { json_schema: { schema: { properties: Record } } }; }; const kullanici = istek.messages.find((m) => m.role === "user")?.content ?? ""; const i = kullanici.indexOf(HAVUZ_ISARETI); const havuz = JSON.parse(kullanici.slice(i + HAVUZ_ISARETI.length)) as Record; const ozellik = istek.response_format.json_schema.schema.properties; const cikti: Record = { genelDegerlendirme: "Liste sıralamana ve seçimlerine göre kuruldu.", uyarilar: ["Tercih dönemi kapanmadan listeyi ÖSYM sistemine kendin gir.", "Her programın koşullarını kılavuzdan kontrol et."], }; atlananOncelikli = 0; for (const l of LISTELER) { const adet = ozellik[l]?.minItems ?? 0; // Kasten: önce öncelikli OLMAYANLAR; yetmezse öncelikliler (havuz tamamen ölçütten oluşuyorsa) const siradan = havuz[l].filter((p) => !p.oncelikli); const oncelikli = havuz[l].filter((p) => p.oncelikli); const secim = [...siradan, ...oncelikli].slice(0, adet); atlananOncelikli += oncelikli.length - secim.filter((p) => p.oncelikli).length; cikti[l] = secim.map((p) => `${p.id}|Sıralamana yakın, alanına uygun bir program.`); } res.writeHead(200, { "Content-Type": "application/json" }); res.end( JSON.stringify({ id: "x", object: "chat.completion", created: 1, model: "sahte", choices: [{ index: 0, message: { role: "assistant", content: JSON.stringify(cikti) }, finish_reason: "stop" }], usage: { prompt_tokens: 1, completion_tokens: 1 }, }), ); }); }); type Rapor = Awaited>; type Senaryo = { ad: string; params: RaporParams; /** AI_API_KEY silinir → dev mock yolu (tani null). */ mock?: boolean; dogrula: (r: Rapor) => string[]; }; /** * İl adları DB biçimindedir ("ANKARA"): sihirbaz ve liste_revize (ilAdiNormalize) * bu biçimi yazar, havuz il süzgeci tam eşleşme yapar; "Ankara" yazılsa süzgeç * boş kalır ve havuz "il gevşet" yoluyla tüm ülkeye açılır (test tuzağı). */ const taban = (revizyon: NonNullable, iller = ["ANKARA", "İZMİR"]): RaporParams => ({ sira: 100000, tur: "say", iller, kategoriler: [], revizyon, }); /** Her senaryoda ortak: 24 satır, dilim adetleri kapsam.dagilim ile aynı, sira 1..n. */ function iskeletDogrula({ rapor }: Rapor): string[] { const h: string[] = []; if (rapor.tercihler.length !== 24) h.push(`satır sayısı ${rapor.tercihler.length} ≠ 24`); const sayac = { hayal: 0, dengeli: 0, garanti: 0 }; for (const t of rapor.tercihler) sayac[t.dilim]++; for (const d of ["hayal", "dengeli", "garanti"] as const) { if (sayac[d] !== rapor.kapsam?.dagilim[d]) h.push(`${d} ${sayac[d]} ≠ dagilim ${rapor.kapsam?.dagilim[d]}`); } rapor.tercihler.forEach((t, i) => { if (t.sira !== i + 1) h.push(`sira ${t.sira} ≠ ${i + 1}`); }); return h; } function ilkNIl(r: Rapor, adet: number, il: string): string[] { const h = iskeletDogrula(r); r.rapor.tercihler.slice(0, adet).forEach((t, i) => { const p = r.rapor.programlar[t.programId]; if (tr(p?.il) !== tr(il)) h.push(`${i + 1}. satır ${p?.il ?? "?"} (${il} bekleniyor)`); }); return h; } function eklenenEsit(r: Rapor, n: number): string[] { return r.tani?.ilkNEklenen === n ? [] : [`tani.ilkNEklenen ${r.tani?.ilkNEklenen} ≠ ${n}`]; } const SENARYOLAR: Senaryo[] = [ { // Bilal'in 7 Eki denemesi. Not: İstanbul her dilimde 20+ programla havuz // kırpmasının öncelikli kotasını tek başına doldurur → havuz 60/60 İstanbul // olur, sahte uç öncelikli olmayan satır bulamaz, takas gerekmez // (ilkNEklenen 0). Garantinin takas yolu "ilk5-takas" senaryosunda ölçülür. ad: "ilk5-istanbul", params: taban({ ilkTercihler: { adet: 5, iller: ["İSTANBUL"] }, not: "ilk 5 İstanbul" }), dogrula: (r) => ilkNIl(r, 5, "İSTANBUL"), }, { // Takas yolu: Yalova 100.000 say'da havuza 0/3/8 (hayal/dengeli/güvenli) // öncelikli satır sokar, her dilimde sıradan satır hedeften fazla kalır → // sahte uç 24'ü önceliklisiz doldurur, kod 5 satırı takasla sokar. ad: "ilk5-takas", params: taban({ ilkTercihler: { adet: 5, iller: ["YALOVA"] }, not: "ilk 5 Yalova" }), dogrula: (r) => [...ilkNIl(r, 5, "YALOVA"), ...eklenenEsit(r, 5)], }, { // Kayseri 100.000 say: 41 programlık havuzda 8 vakıf (1/0/7), sıradan her // dilimde hedeften fazla → sahte uç hep devlet seçer, kod 3 vakıf takaslar. ad: "ilk3-vakif", params: taban({ ilkTercihler: { adet: 3, universiteTipi: "vakif" }, not: "ilk 3 vakıf" }, ["KAYSERİ"]), dogrula: (r) => { const h = iskeletDogrula(r); r.rapor.tercihler.slice(0, 3).forEach((t, i) => { const p = r.rapor.programlar[t.programId]; if (p?.unitur === "DEVLET") h.push(`${i + 1}. satır devlet (vakıf bekleniyor)`); }); return [...h, ...eklenenEsit(r, 3)]; }, }, { // Ardahan 100.000 say'da havuza yalnız 2 program sokar: eldeki kadarı başta, hata yok ad: "havuz-yetersiz", params: taban({ ilkTercihler: { adet: 10, iller: ["ARDAHAN"] }, not: "ilk 10 Ardahan" }), dogrula: (r) => { const h = iskeletDogrula(r); const n = r.tani?.ilkNEklenen ?? -1; if (n < 0 || n > 10) h.push(`tani.ilkNEklenen ${n} 0..10 dışında`); const ardahan = r.rapor.tercihler.filter((t) => tr(r.rapor.programlar[t.programId]?.il) === "ardahan").length; const bastaki = r.rapor.tercihler.findIndex((t) => tr(r.rapor.programlar[t.programId]?.il) !== "ardahan"); if (bastaki !== -1 && bastaki !== Math.min(ardahan, 10)) h.push(`Ardahan satırları başta toplu değil (${ardahan} satır, ilk uymayan ${bastaki + 1}.)`); return h; }, }, { // Dev mock yolu (AI_API_KEY yok): mock dilim içinde öncelikliyi önce alır ve // sonunda ilkTercihleriOneAl uygular; Yalova params'ı ile ilk 5 Yalova olmalı. ad: "mock-yolu", mock: true, params: taban({ ilkTercihler: { adet: 5, iller: ["YALOVA"] }, not: "ilk 5 Yalova" }), dogrula: (r) => { const h = ilkNIl(r, 5, "YALOVA"); if (r.tani !== null) h.push("mock yolunda tani null olmalı"); if (!r.rapor.devMock) h.push("devMock işareti yok"); return h; }, }, ]; async function main() { await new Promise((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.OPENROUTER_API_KEY; delete process.env.TYPESAFE_API_KEY; // Jev'e gitme const { raporUretAyrintili } = await import("@/lib/ai/rapor"); const info = console.info; console.info = () => {}; let hata = 0; for (const s of SENARYOLAR) { if (s.mock) delete process.env.AI_API_KEY; // mockAktif(): anahtar yok + NODE_ENV ≠ production let hatalar: string[]; let ozet = ""; try { const r = await raporUretAyrintili(s.params, s.params.revizyon?.not, undefined); hatalar = s.dogrula(r); const ilk5 = r.rapor.tercihler.slice(0, 5).map((t) => { const p = r.rapor.programlar[t.programId]; return `${p?.il ?? "?"}/${p?.unitur === "DEVLET" ? "D" : "V"}`; }); ozet = `satır ${r.rapor.tercihler.length}, ilkNEklenen ${r.tani?.ilkNEklenen ?? "—"}, sahte uç atladı ${s.mock ? "—" : atlananOncelikli}, ilk 5: ${ilk5.join(" ")}`; } catch (e) { hatalar = [`hata fırladı: ${(e as Error).message}`]; } if (hatalar.length) hata++; info(`\n${hatalar.length ? "KALDI" : "geçti"} [${s.ad}]\n ${ozet}${hatalar.map((h) => `\n ✘ ${h}`).join("")}`); } 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); });