From 0b3bb8223f30805d6aa77dc199b53f96edeb36cb Mon Sep 17 00:00:00 2001 From: ht Date: Sun, 6 Sep 2026 10:09:12 +0300 Subject: [PATCH] Require real draft evidence for offer audit findings --- docs/quality/uygulama-durumu.md | 10 ++++++++++ server/ai-teklif.mjs | 13 +++--------- server/offer-audit.mjs | 33 +++++++++++++++++++++++++++++++ src/components/TeklifAsistani.jsx | 5 +++-- tests/offer-audit.test.mjs | 25 +++++++++++++++++++++++ 5 files changed, 74 insertions(+), 12 deletions(-) create mode 100644 server/offer-audit.mjs create mode 100644 tests/offer-audit.test.mjs diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md index 7a8db25..e686522 100644 --- a/docs/quality/uygulama-durumu.md +++ b/docs/quality/uygulama-durumu.md @@ -431,3 +431,13 @@ Kod incelemesinde Taslak Üret akışının belge listesini bekledikten sonra g Teklif Asistanı durumu hesap, token oturumu veya İKN değiştiğinde yeniden oluşturulur. Bileşen kapanınca dosya okuma/AI sonucu işlenmez. Eşzamanlı düğme ve dosya işlemleri kilitlenir. Yeni kaynak dosyası yüklenince önceki yanıt/denetim temizlenir; kullanıcının düzenlediği taslak bölümleri korunur. Doğrulama: dört yeni test; belge listesi sırasında hesap değişiminde sıfır AI isteği, envanter hatasında sıfır AI isteği, doğru belge kapsamı ve başlangıç yetkilendirmesi, geç yanıtta hesap/görünüm değişimi. Tüm 87 test geçti. Üretim derlemesi başarılı; büyük paket uyarıları mevcut. Gerçek AI sağlayıcısı ve iki hesaplı tarayıcı kabulü bu turda yapılmadı. D01–D11 kapsamının tamamı halen açık. + +### D06 — Teklif denetiminde kaynak kanıtı ve belirsizlik puanı + +Mevcut teklif denetimi model maddelerini dizi sırasıyla gerekliliklere atıyor, modelin `karsilaniyor` kararını taslak alıntısını doğrulamadan kabul ediyor ve belirsiz maddelere yarım puan veriyordu. Bunlar yüksek uygunluk oranının yanlış algılanmasına yol açabiliyordu. + +Yeni sunucu doğrulayıcısı gerekliliği tam metin eşleşmesiyle tekil olarak bulur; sırası değişmiş yanıtlar doğru maddeye bağlanır. Eksik, değiştirilmiş veya yinelenmiş kimlikler belirsizdir. Karşılanıyor kararında alıntı taslakta (Unicode/boşluk normalleştirmesi dışında değiştirilmeden) bulunmalıdır; yoksa karar belirsiz olur ve uydurma alıntı gösterilmez. Belirsiz satırlar yüzdeye puan eklemez. Özet modelin serbest iddiasından değil doğrulanmış satır sayılarından üretilir. Boş/bozuk üst yanıt hata verir. + +Ekran oranı yalnız listedeki alıntıyla desteklenen maddeler olarak açıklar; kontrol gereken madde sayısını ve resmi uygunluk/tüm doküman kabulü olmadığını gösterir. Alıntının taslakta bulunması, anlam bakımından şartın karşılandığını tek başına kanıtlamaz. Gereklilik toplamadaki mevcut örnekleme/satır sınırları ve tam doküman kapsamı hâlâ geliştirme gerektiriyor; bu değişiklik tam mevzuat/semantik doğrulama değildir. + +Doğrulama: uydurma kanıt, eksik madde, yeniden sıralanmış satır, yinelenmiş/değişmiş gereklilik ve bozuk yanıt için dört yeni test. Tüm 91 test ve üretim derlemesi geçti. Gerçek sağlayıcıyla uçtan uca denetim kabulü yapılmadı. Büyük paket uyarıları ve planın diğer açık işleri sürüyor. diff --git a/server/ai-teklif.mjs b/server/ai-teklif.mjs index a690acb..752d938 100644 --- a/server/ai-teklif.mjs +++ b/server/ai-teklif.mjs @@ -1,3 +1,4 @@ +import { qualifyOfferAudit } from './offer-audit.mjs'; import { documentContent } from './document-content.mjs'; // Teklif asistanı — İhaleAB'den taşınan üç AI özelliği, ihaleist'in mevcut // analyzeSpec zinciri (yedekli sağlayıcılar) üzerinde: @@ -101,17 +102,9 @@ export async function teklifDenetle({ ikn, taslak, gereklilikler, tender, chain, const { text: metin } = await ihaleMetni(ikn, text, tender?.aciklama).catch(() => ({ text: '' })); const liste = gereklilikMetni(tender, gereklilikler, metin); if (!liste.length) throw new Error('Gereklilik listesi yok — önce derin analiz çalıştırın veya dokümanı yükleyin.'); - const sistem = `Sen kamu ihalelerinde teklif dosyası denetçisisin. Gereklilik listesi ve teklif TASLAĞI verilecek. Her gereklilik için taslağı kontrol et. "karsilaniyor" yalnızca taslakta açık karşılığı varsa (alıntıyı 'kanit'e aynen kopyala); değinilmiyorsa "eksik"; kısmen/belirsizse "belirsiz". Sayı üretme. Yalnızca JSON: {"ozet":"2-3 cümle","maddeler":[{"gereklilik":"","durum":"karsilaniyor"|"eksik"|"belirsiz","kanit":"veya null","oneri":"veya null"}]} — maddeleri verilen sırada döndür.`; + const sistem = `Sen kamu ihalelerinde teklif dosyası denetçisisin. Gereklilik listesi ve teklif TASLAĞI verilecek. Her gereklilik için taslağı kontrol et. "karsilaniyor" yalnızca taslakta açık karşılığı varsa (alıntıyı 'kanit'e aynen kopyala); değinilmiyorsa "eksik"; kısmen/belirsizse "belirsiz". Sayı üretme. Yalnızca JSON: {"ozet":"2-3 cümle","maddeler":[{"gereklilik":"","durum":"karsilaniyor"|"eksik"|"belirsiz","kanit":"veya null","oneri":"veya null"}]} — maddeleri verilen sırada döndür; gereklilik alanına verilen gereklilik metnini değiştirmeden aynen kopyala.`; const out = await analyzeSpec({ text: `GEREKLİLİKLER:\n${liste.map((g, i) => `${i + 1}. ${g}`).join('\n')}\n\nTEKLİF TASLAĞI:\n${t}\n\nYalnızca JSON döndür.`, tender, chain, systemOverride: sistem, mode: 'derin' }); - const ham = Array.isArray(out?.result?.maddeler) ? out.result.maddeler : []; - const DUR = new Set(['karsilaniyor', 'eksik', 'belirsiz']); - const maddeler = liste.map((g, i) => { - const m = ham[i] ?? {}; - return { gereklilik: g, durum: DUR.has(m.durum) ? m.durum : 'belirsiz', kanit: m.kanit ? String(m.kanit).slice(0, 400) : null, oneri: m.oneri ? String(m.oneri).slice(0, 400) : null }; - }); - const ok = maddeler.filter((m) => m.durum === 'karsilaniyor').length; - const yarim = maddeler.filter((m) => m.durum === 'belirsiz').length; - return { ozet: String(out?.result?.ozet ?? '').slice(0, 800), maddeler, uyum_yuzde: Math.round(((ok + yarim * 0.5) / maddeler.length) * 100), eksikler: maddeler.filter((m) => m.durum === 'eksik').map((m) => m.gereklilik) }; + return qualifyOfferAudit(out?.result, liste, t); } const BOLUMLER = { diff --git a/server/offer-audit.mjs b/server/offer-audit.mjs new file mode 100644 index 0000000..f66d627 --- /dev/null +++ b/server/offer-audit.mjs @@ -0,0 +1,33 @@ +const normalize = text => String(text ?? '').normalize('NFC').replace(/\s+/g, ' ').trim(); +const optionalText = value => typeof value === 'string' && value.trim() ? value.trim() : null; + +/** A model cannot certify a row without identifying it and quoting the actual draft. */ +export function qualifyOfferAudit(result, requirements, draft) { + if (!result || !Array.isArray(result.maddeler) || !result.maddeler.length) + throw new Error('Model denetim maddelerini döndürmedi. Yeniden deneyin.'); + const source = normalize(draft); + const maddeler = requirements.map(gereklilik => { + const matches = result.maddeler.filter(row => row && typeof row.gereklilik === 'string' && normalize(row.gereklilik) === normalize(gereklilik)); + const row = matches.length === 1 ? matches[0] : null; + let durum = ['karsilaniyor','eksik','belirsiz'].includes(row?.durum) ? row.durum : 'belirsiz'; + let kanit = optionalText(row?.kanit); + let oneri = optionalText(row?.oneri); + if (!row) oneri = 'Model yanıtı bu gereklilikle tekil olarak eşleştirilemedi. Kaynak ve taslağı karşılaştırın.'; + if (durum === 'karsilaniyor' && (!kanit || !source.includes(normalize(kanit)))) { + durum = 'belirsiz'; + oneri = 'Karşılandığı iddiası taslaktaki gerçek bir alıntıyla doğrulanamadı. Elle kontrol edin.'; + } + // Never display invented text as a quote from the user's draft. + if (kanit && !source.includes(normalize(kanit))) kanit = null; + return {gereklilik, durum, kanit, oneri}; + }); + const verified = maddeler.filter(row => row.durum === 'karsilaniyor').length; + const uncertain = maddeler.filter(row => row.durum === 'belirsiz').length; + return { + ozet: `${maddeler.length} gereklilikten ${verified} tanesi taslak alıntısıyla destekleniyor; ${uncertain} tanesi elle kontrol gerektiriyor.`, maddeler, + uyum_yuzde: maddeler.length ? Math.round(verified / maddeler.length * 100) : 0, + eksikler: maddeler.filter(row => row.durum === 'eksik').map(row => row.gereklilik), + belirsizSayisi: uncertain, + kapsam: 'Yalnız bu listede gösterilen gereklilikler kontrol edildi. Oran resmi başvuru uygunluğu veya tüm dokümanın eksiksiz denetlendiği anlamına gelmez. Alıntının taslakta bulunması şartın doğru karşılandığını tek başına kanıtlamaz.', + }; +} diff --git a/src/components/TeklifAsistani.jsx b/src/components/TeklifAsistani.jsx index 8f64135..1bd785a 100644 --- a/src/components/TeklifAsistani.jsx +++ b/src/components/TeklifAsistani.jsx @@ -123,9 +123,10 @@ const OfferAssistantSession = ({ tender }) => { {denetim && (
- %{denetim.uyum_yuzde} - {denetim.maddeler.length} gereklilik · {denetim.eksikler.length} eksik + %{denetim.uyum_yuzde} + {denetim.maddeler.length} gereklilik · {denetim.eksikler.length} eksik · {denetim.belirsizSayisi ?? 0} kontrol gerekli
+

Listede alıntıyla desteklenen karşılanma oranı. {denetim.kapsam}

{denetim.ozet &&

{denetim.ozet}

}