v2 #10

Merged
bilal merged 34 commits from v2 into main 2026-10-11 01:38:50 +03:00
2 changed files with 61 additions and 7 deletions
Showing only changes of commit f955a76c1e - Show all commits

View File

@@ -5,8 +5,9 @@
*
* 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.
* uydurma slug düz metne iner; iç içe `[[x](y)](y)` bağlantısı (her adres)
* `[x](y)` olur; 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";
@@ -71,6 +72,19 @@ function senaryolar(rehberler: readonly RehberKaydi[]): Senaryo[] {
bekle: `Oku: [${obp.baslik}](/rehber/${obp.slug}).`,
duzeltme: 1,
},
{
// QA 8 Eki 2026: paketsiz cevapta modelin ürettiği biçim
ad: "ic-ice-paket-baglantisi",
metin: "Tercih Dönemi Paketi (299 TL, [[/paket](/paket)](/paket)) ile açılır; [[a](/x)](/y) kalır.",
bekle: "Tercih Dönemi Paketi (299 TL, [/paket](/paket)) ile açılır; [[a](/x)](/y) kalır.",
duzeltme: 0,
},
{
ad: "ic-ice-rehber-baglantisi-sonra-eslesir",
metin: "Oku: [[Yatay geçiş](/rehber/yatay-gecis-rehberi)](/rehber/yatay-gecis-rehberi) ve [[[OBP](/rehber/obp-nedir)](/rehber/obp-nedir)](/rehber/obp-nedir).",
bekle: `Oku: [${yatay.baslik}](/rehber/${yatay.slug}) ve [${obp.baslik}](/rehber/${obp.slug}).`,
duzeltme: 2,
},
];
}

View File

@@ -19,6 +19,14 @@
*
* `/universite/…` ve `/bolum/…` bu turda doğrulanmaz (backlog).
*
* İç içe bağlantı (8 Eki 2026, QA): model bazen `[[/paket](/paket)](/paket)`
* gibi aynı adresi iki kez saran bozuk markdown yazıyor; ekranda köşeli
* parantezler ve "Paket (Paket)" kartı görünüyordu. Adres ne olursa olsun
* (`/rehber/` ile sınırlı değil) `[[x](y)](y)` → `[x](y)` düzleştirilir; bu
* düzeltme rehber eşlemesinden ÖNCE yapılır ki içteki rehber adresi de
* doğrulansın. Tampon bunun için bir bağlantı adayının `)`'sinden sonra
* `](` gelip gelmediğine bakar (bir karakterlik bekleme).
*
* Saf modül: DB/ağ/Next yok, `server-only` import etmez; akış modülü gibi
* betiklerden de koşar.
*/
@@ -59,6 +67,25 @@ const BAGLANTI = new RegExp(
"g",
);
/**
* Aynı adresi iki kez saran iç içe markdown bağlantısı: `[[x](y)](y)`.
* Etiket köşeli parantez, adres yuvarlak parantez içermez; adres iki
* katmanda aynıysa eşleşir (farklı adresler düzleştirilmez — hangisi
* doğru bilinemez).
*/
const IC_ICE_BAGLANTI = /\[\[([^[\]]*)\]\(([^()]*)\)\]\(\2\)/g;
/** `[[x](y)](y)` → `[x](y)`; daha derin sarmalar için kalmayana dek yinelenir. */
export function icIceBaglantiyiDuzlestir(metin: string): string {
let onceki: string;
let simdiki = metin;
do {
onceki = simdiki;
simdiki = onceki.replace(IC_ICE_BAGLANTI, "[$1]($2)");
} while (simdiki !== onceki);
return simdiki;
}
function parcalar(slug: string): Set<string> {
return new Set(slug.split("-").filter(Boolean));
}
@@ -95,8 +122,10 @@ export function enYakinRehber(slug: string, rehberler: readonly RehberKaydi[]):
}
/**
* Metindeki `/rehber/<slug>` adreslerini (markdown bağlantısı, ters tırnak ve
* çıplak adres) gerçek kayıtlarla eşler. Kayıtlı slug dokunulmaz; yakın
* Önce iç içe bağlantılar düzleştirilir (`[[x](y)](y)` → `[x](y)`, her adres;
* `duzeltmeler`e yazılmaz — rehber kaydı değil biçim hatası), sonra
* metindeki `/rehber/<slug>` adresleri (markdown bağlantısı, ters tırnak ve
* çıplak adres) gerçek kayıtlarla eşlenir. Kayıtlı slug dokunulmaz; yakın
* kayıt varsa `[başlık](/rehber/yeni-slug)` olur (başlık kayıttan); yoksa
* markdown bağlantısı etiketine iner, ters tırnak içi ve çıplak adres
* silinir ("bkz." gibi öncesindeki bağlaç kalır).
@@ -107,7 +136,7 @@ export function rehberBaglantilariniDuzelt(
): { metin: string; duzeltmeler: BaglantiDuzeltme[] } {
const duzeltmeler: BaglantiDuzeltme[] = [];
const kayitli = new Set(rehberler.map((r) => r.slug));
const yeni = metin.replace(BAGLANTI, (eslesme, etiket: string | undefined, mdSlug?: string, tirnakSlug?: string, ciplakSlug?: string) => {
const yeni = icIceBaglantiyiDuzlestir(metin).replace(BAGLANTI, (eslesme, etiket: string | undefined, mdSlug?: string, tirnakSlug?: string, ciplakSlug?: string) => {
const slug = mdSlug ?? tirnakSlug ?? ciplakSlug ?? "";
if (kayitli.has(slug)) return eslesme;
const yakin = enYakinRehber(slug, rehberler);
@@ -203,9 +232,20 @@ export class BaglantiTamponu {
if (kapanis === -1 || kapanis > ETIKET_UST + 1) return m.length > ETIKET_UST + 1 ? "degil" : "bekle";
if (kapanis + 1 >= m.length) return "bekle";
if (m[kapanis + 1] !== "(") return "degil";
const parantez = m.indexOf(")", kapanis + 2);
let parantez = m.indexOf(")", kapanis + 2);
if (parantez === -1) return m.length - (kapanis + 2) > ADRES_UST ? "degil" : "bekle";
return parantez + 1;
// Kapanan bağlantıyı bir dış bağlantı sarıyor olabilir (`[[x](y)](y)`,
// icIceBaglantiyiDuzlestir): `)`'den sonra `](` geliyorsa dış adresin
// kapanışına dek tutulur; sonraki karakter henüz gelmediyse beklenir.
for (;;) {
if (parantez + 1 >= m.length) return "bekle";
if (m[parantez + 1] !== "]") return parantez + 1;
if (parantez + 2 >= m.length) return "bekle";
if (m[parantez + 2] !== "(") return parantez + 1;
const dis = m.indexOf(")", parantez + 3);
if (dis === -1) return m.length - (parantez + 3) > ADRES_UST ? parantez + 1 : "bekle";
parantez = dis;
}
}
if (m[0] === "`") {
const kapanis = m.indexOf("`", 1);