- 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>
440 lines
18 KiB
TypeScript
440 lines
18 KiB
TypeScript
/**
|
||
* 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;
|
||
}
|