v2 #10

Merged
bilal merged 34 commits from v2 into main 2026-10-11 01:38:50 +03:00
Showing only changes of commit 71294e1d5d - Show all commits

View File

@@ -0,0 +1,214 @@
/**
* 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<string, { minItems?: number }> } } };
};
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<TelListe, { id: string; oncelikli?: boolean }[]>;
const ozellik = istek.response_format.json_schema.schema.properties;
const cikti: Record<string, unknown> = {
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<ReturnType<typeof import("@/lib/ai/rapor").raporUretAyrintili>>;
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<RaporParams["revizyon"]>, 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<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.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);
});