diff --git a/scripts/eval/akis-denetim.ts b/scripts/eval/akis-denetim.ts index 2da1069..edc51b8 100644 --- a/scripts/eval/akis-denetim.ts +++ b/scripts/eval/akis-denetim.ts @@ -7,16 +7,24 @@ * 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ı. Model davranışını ÖLÇMEZ; onu eval:sohbet ölçer. + * 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). Model davranışını + * ÖLÇMEZ; onu eval:sohbet ö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 }; -type Senaryo = { ad: string; beklenen: Beklenen; adimlar: ({ metin: string } | { araclar: { name: string; arguments: string }[] })[] }; -const B = (vaatNotu: boolean, kayitNotu: boolean, reddedilenArac = 0): Beklenen => ({ vaatNotu, kayitNotu, reddedilenArac }); +type Beklenen = { vaatNotu: boolean; kayitNotu: boolean; reddedilenArac: number; baglantiDuzeltme: number }; +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[] }; +}; +const B = (vaatNotu: boolean, kayitNotu: boolean, reddedilenArac = 0, baglantiDuzeltme = 0): Beklenen => ({ vaatNotu, kayitNotu, reddedilenArac, baglantiDuzeltme }); 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." }] }, @@ -25,6 +33,19 @@ const SENARYOLAR: Senaryo[] = [ { 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"] }, + }, ]; let aktif: Senaryo = SENARYOLAR[0]; let adimNo = 0; @@ -61,6 +82,9 @@ async function main() { 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; @@ -76,6 +100,7 @@ async function main() { system: "test", mesajlar: [{ role: "user", content: "test" }], aracBaglami: { userId, sira: 100000, tur: "say", profil: {} }, + rehberler, onTamam: async (m) => { tam = m; }, @@ -91,7 +116,9 @@ async function main() { if (v.type === "tool-output-available") araclar.push(JSON.stringify(v.output).slice(0, 70)); } const d = denetim as Beklenen | null; - const tuttu = d != null && d.vaatNotu === s.beklenen.vaatNotu && d.kayitNotu === s.beklenen.kayitNotu && d.reddedilenArac === s.beklenen.reddedilenArac; + const kayit = String(tam); + const metinTuttu = (s.metinde?.var ?? []).every((m) => kayit.includes(m)) && (s.metinde?.yok ?? []).every((m) => !kayit.includes(m)); + const tuttu = d != null && d.vaatNotu === s.beklenen.vaatNotu && d.kayitNotu === s.beklenen.kayitNotu && d.reddedilenArac === s.beklenen.reddedilenArac && d.baglantiDuzeltme === s.beklenen.baglantiDuzeltme && metinTuttu; if (!tuttu) hata++; info(`\n${tuttu ? "geçti" : "KALDI"} [${s.ad}]\n denetim: ${JSON.stringify(denetim)}\n araç çıktıları: ${araclar.length} → ${araclar.join(" | ")}\n son metin: ${String(tam).replace(/\n+/g, " ⏎ ")}`); } diff --git a/scripts/eval/baglanti-denetim.ts b/scripts/eval/baglanti-denetim.ts new file mode 100644 index 0000000..0891570 --- /dev/null +++ b/scripts/eval/baglanti-denetim.ts @@ -0,0 +1,127 @@ +/** + * Rehber bağlantısı doğrulamasının MEKANİK testi — parasız, anahtarsız, DB'siz. + * + * pnpm tsx --conditions=react-server scripts/eval/baglanti-denetim.ts + * + * lib/ai/baglanti-denetimi.ts gerçek content/rehber listesiyle sınanır: + * kayıtlı slug dokunulmaz, yakın slug yeniden yazılır (başlık kayıttan), + * uydurma slug düz metne iner; akış tamponunun parça parça çıktısı tek + * seferlik sonuçla birebir aynı olmalı. Beklenti tutmazsa çıkış kodu 1. + */ +import { BaglantiTamponu, rehberBaglantilariniDuzelt, type RehberKaydi } from "@/lib/ai/baglanti-denetimi"; +import { getRehberAramaKayitlari } from "@/lib/rehber"; + +type Senaryo = { + ad: string; + metin: string; + /** Beklenen tam çıktı (string) ya da çıktıyı denetleyen fonksiyon. */ + bekle: string | ((cikti: string) => boolean); + duzeltme: number; +}; + +function senaryolar(rehberler: readonly RehberKaydi[]): Senaryo[] { + const kayit = (slug: string) => { + const r = rehberler.find((x) => x.slug === slug); + if (!r) throw new Error(`content/rehber içinde yok: ${slug}`); + return r; + }; + const yatay = kayit("yatay-gecis-cap-yandal"); + const obp = kayit("obp-nedir-nasil-hesaplanir"); + const gercek = kayit("devlet-mi-vakif-mi"); + return [ + { + ad: "gercek-slug-aynen", + metin: `Şu yazıya bak: [${gercek.baslik}](/rehber/${gercek.slug}) — orada ayrıntı var.`, + bekle: `Şu yazıya bak: [${gercek.baslik}](/rehber/${gercek.slug}) — orada ayrıntı var.`, + duzeltme: 0, + }, + { + ad: "yakin-eslesme-markdown", + metin: "Detay için [Yatay geçiş](/rehber/yatay-gecis-rehberi) yazısına bak.", + bekle: `Detay için [${yatay.baslik}](/rehber/${yatay.slug}) yazısına bak.`, + duzeltme: 1, + }, + { + ad: "uydurma-duz-metin", + metin: "Bunu [Tamamen uydurma yazı](/rehber/tamamen-uydurma-seyler) anlatıyor.", + bekle: "Bunu Tamamen uydurma yazı anlatıyor.", + duzeltme: 1, + }, + { + ad: "ters-tirnak-ve-ciplak", + metin: "Bkz. `/rehber/obp-nedir` ve ayrıca /rehber/yatay-gecis-rehberi. Uydurma: /rehber/tamamen-uydurma-seyler bitti.", + bekle: `Bkz. [${obp.baslik}](/rehber/${obp.slug}) ve ayrıca [${yatay.baslik}](/rehber/${yatay.slug}). Uydurma: bitti.`, + duzeltme: 3, + }, + { + ad: "koseli-parantez-kayipsiz", + metin: "[2024] verisi ve [not] şöyle; `kod` de olduğu gibi kalır.", + bekle: "[2024] verisi ve [not] şöyle; `kod` de olduğu gibi kalır.", + duzeltme: 0, + }, + { + ad: "baska-yol-dokunulmaz", + metin: "[Ankara Üniversitesi](/universite/ankara-universitesi) ve [Tıp](/bolum/tip) kalır.", + bekle: "[Ankara Üniversitesi](/universite/ankara-universitesi) ve [Tıp](/bolum/tip) kalır.", + duzeltme: 0, + }, + { + ad: "site-koku-ile-markdown", + metin: "Oku: [OBP](https://kolaytercih.com/rehber/obp-nedir).", + bekle: `Oku: [${obp.baslik}](/rehber/${obp.slug}).`, + duzeltme: 1, + }, + ]; +} + +/** Metni verilen parça sayısına rastgele olmayan, sabit noktalardan böler. */ +function parcala(metin: string, adet: number): string[] { + const boy = Math.max(1, Math.ceil(metin.length / adet)); + const parcalar: string[] = []; + for (let i = 0; i < metin.length; i += boy) parcalar.push(metin.slice(i, i + boy)); + return parcalar; +} + +function main() { + const rehberler = getRehberAramaKayitlari(); + if (rehberler.length < 10) throw new Error(`rehber listesi şüpheli derecede kısa: ${rehberler.length}`); + let hata = 0; + const kontrol = (ad: string, tuttu: boolean, detay: string) => { + if (!tuttu) hata++; + console.log(`${tuttu ? "geçti" : "KALDI"} [${ad}]${tuttu ? "" : `\n ${detay}`}`); + }; + for (const s of senaryolar(rehberler)) { + const tek = rehberBaglantilariniDuzelt(s.metin, rehberler); + const beklendi = typeof s.bekle === "string" ? tek.metin === s.bekle : s.bekle(tek.metin); + kontrol(s.ad, beklendi && tek.duzeltmeler.length === s.duzeltme, `çıktı: ${tek.metin}\n düzeltme: ${JSON.stringify(tek.duzeltmeler)}`); + // Tampon: metin 1, 3, 4, 7 ve karakter karakter parçalanınca da aynı sonuç + for (const adet of [1, 3, 4, 7, s.metin.length]) { + const tampon = new BaglantiTamponu(rehberler); + let akis = ""; + for (const p of parcala(s.metin, adet)) akis += tampon.besle(p); + akis += tampon.bitir(); + kontrol( + `${s.ad} · tampon ${adet} parça`, + akis === tek.metin && tampon.duzeltmeler.length === tek.duzeltmeler.length, + `tampon: ${akis}\n tek seferlik: ${tek.metin}`, + ); + } + } + // Tampon bir bağlantı adayını kapanmadan bırakmamalı (yanlış hap ekrana gitmesin) + { + const tampon = new BaglantiTamponu(rehberler); + const ilk = tampon.besle("Bak: [Yatay geçiş](/rehber/yatay-gecis"); + const son = tampon.besle("-rehberi) iyi.") + tampon.bitir(); + kontrol("tampon-adayi-tutuyor", ilk === "Bak: " && !ilk.includes("/rehber/") && son.includes("/rehber/yatay-gecis-cap-yandal"), `ilk: ${ilk}\n son: ${son}`); + } + { + const tampon = new BaglantiTamponu(rehberler); + const ilk = tampon.besle("Ayrıntı /reh"); + const son = tampon.besle("ber/obp-nedir yazısında.") + tampon.bitir(); + kontrol("tampon-yarim-onek", ilk === "Ayrıntı " && son.includes("/rehber/obp-nedir-nasil-hesaplanir"), `ilk: ${ilk}\n son: ${son}`); + } + const toplam = senaryolar(rehberler).length * 6 + 2; + console.log(`\n${toplam - hata}/${toplam} senaryo geçti (mekanik test; gerçek model davranışını ölçmez)`); + process.exit(hata ? 1 : 0); +} +main(); diff --git a/src/app/api/soru/route.ts b/src/app/api/soru/route.ts index 3c1d018..70806e6 100644 --- a/src/app/api/soru/route.ts +++ b/src/app/api/soru/route.ts @@ -22,8 +22,8 @@ import { type RevizyonOzeti, type SohbetMeta, } from "@/lib/ai/sohbet-meta"; -import { MAX_REVIZYON } from "@/lib/credits"; import { profilGetir } from "@/lib/danisman-profil-db"; +import { getRehberAramaKayitlari } from "@/lib/rehber"; import { listeOzetiCikar } from "@/lib/ai/rapor"; import type { RaporSonuc } from "@/features/rapor/types/rapor"; import type { RaporParams } from "@/features/rapor/types/rapor"; @@ -196,7 +196,7 @@ export async function POST(request: NextRequest) { uyarilar: raporSonuc.uyarilar, kapsam: raporSonuc.kapsam ? { toplam: raporSonuc.kapsam.toplam, uyarlandi: raporSonuc.kapsam.uyarlandi } : undefined, }, - revizyon: { kalanHak: Math.max(0, MAX_REVIZYON - rapor.revisionCount), paketli }, + revizyon: { paketli }, }); // Araçların bağlamı: aday sırası/puan türü (arama süzgeci), profil // (profil_guncelle yazınca aynı tur içinde tazelenir) ve sihirbaz seçimleri @@ -207,7 +207,6 @@ export async function POST(request: NextRequest) { tur: raporParams.tur, hasPaket: paketli, mesajId: kullaniciMesajId, - kalanRevizyon: Math.max(0, MAX_REVIZYON - rapor.revisionCount), profil, oncelikler: raporParams.oncelikler ?? [], ozelDurum: raporParams.ozelDurum ?? [], @@ -297,6 +296,9 @@ export async function POST(request: NextRequest) { system, mesajlar, aracBaglami, + // Cevaptaki /rehber/ adresleri gerçek yazı listesiyle doğrulanır + // (prod'da cache'li, lib/rehber.ts) + rehberler: getRehberAramaKayitlari(), onAracSonucu: (ad, cikti) => { const c = cikti as Record; if (ad === "soru_sor" && c.anket) cevapMeta.anket = c.anket as SohbetMeta["anket"]; diff --git a/src/app/page.tsx b/src/app/page.tsx index 99d0104..ce82edb 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -28,7 +28,7 @@ import { SectionEyebrow, } from "@/components/pixel-decor"; import { PikselCtaYuzeyi } from "@/components/piksel-cta-yuzeyi"; -import { MAX_REVIZYON, URUNLER } from "@/lib/credits"; +import { URUNLER } from "@/lib/credits"; import { PROGRAM_SAYISI_CUMLE, PROGRAM_SAYISI_METNI, @@ -244,7 +244,7 @@ const paketOzellikleri = [ text: `Her tercih için yerleşme riski ve trend analizi (son ${VERI_YIL_SAYISI} yıl)`, }, { icon: FileText, text: "Veliyle paylaşılabilir PDF rapor" }, - { icon: RefreshCcw, text: `${MAX_REVIZYON} liste revizyon hakkı` }, + { icon: RefreshCcw, text: "Sınırsız liste revizyonu (danışmanla sohbetten)" }, { icon: MessageCircleQuestion, text: `${URUNLER.paket.credits} yapay zekâ danışman sorusu`, diff --git a/src/features/odeme/components/paket-satinal.tsx b/src/features/odeme/components/paket-satinal.tsx index b8e0f49..2011579 100644 --- a/src/features/odeme/components/paket-satinal.tsx +++ b/src/features/odeme/components/paket-satinal.tsx @@ -34,7 +34,7 @@ const OZELLIKLER = [ text: `Her tercih için yerleşme riski ve trend analizi (son ${VERI_YIL_SAYISI} yıl)`, }, { icon: FileText, text: "Veliyle paylaşılabilir PDF rapor" }, - { icon: RefreshCcw, text: "2 liste revizyon hakkı" }, + { icon: RefreshCcw, text: "Sınırsız liste revizyonu (danışmanla sohbetten)" }, { icon: MessageCircleQuestion, text: `${URUNLER.paket.credits} Yapay Zeka danışman sorusu (her mesaj 1 kredi)`, diff --git a/src/features/rapor/components/listem-govde.tsx b/src/features/rapor/components/listem-govde.tsx index 0aa55a6..3943711 100644 --- a/src/features/rapor/components/listem-govde.tsx +++ b/src/features/rapor/components/listem-govde.tsx @@ -75,7 +75,6 @@ export function ListemGovde({ danismanProfili, ozet, ilkYorum, - kalanHak, paketFiyati, paketKredi, }: { @@ -99,7 +98,6 @@ export function ListemGovde({ ozet?: DanismanOzeti; /** Sohbet boşken açılışta gösterilen 1. tercih yorumu */ ilkYorum?: IlkTercihYorumu; - kalanHak: number; paketFiyati: string; paketKredi: number; }) { @@ -216,12 +214,12 @@ export function ListemGovde({ } if (hasPaket) { satirlar.push( - `Listende değişiklik isteyebilirsin: danışman gerekirse kısa bir formla sorar, sonra listeyi yeniden kurar (kalan hak: ${kalanHak})`, + `Listende değişiklik isteyebilirsin: danışman gerekirse kısa bir formla sorar, sonra listeyi yeniden kurar`, ); } satirlar.push("Bu sohbetteki son mesajların"); return satirlar; - }, [adaySira, params, hasPaket, acikSatir, kalanHak]); + }, [adaySira, params, hasPaket, acikSatir]); // Kapı ölçümü: paywall yüzeyleri viewport'a girince (oturumda 1'er kez). // Ref'ler yalnızca paketsiz dallarda bağlanır; paketlide DOM'da olmadıkları @@ -331,18 +329,16 @@ export function ListemGovde({ {/* Revizyon sohbette yaşar (7 Eki 2026): düğme danışman kutusunu odaklar, aday ne değişsin istediğini yazar; danışman gerekirse soru formuyla netleştirip listeyi - yeniden kurar. Hak bitince düğme kalkar. */} - {kalanHak > 0 ? ( - - ) : null} + yeniden kurar. Sayı sınırı yok (7 Eki 2026). */} +