Require real draft evidence for offer audit findings

This commit is contained in:
2026-09-06 10:09:12 +03:00
parent ecc031a822
commit 0b3bb8223f
5 changed files with 74 additions and 12 deletions
+10
View File
@@ -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.
+3 -10
View File
@@ -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 = {
+33
View File
@@ -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.',
};
}
+3 -2
View File
@@ -123,9 +123,10 @@ const OfferAssistantSession = ({ tender }) => {
{denetim && (
<div className="space-y-2 rounded-xl border border-slate-200 p-3">
<div className="flex items-baseline gap-3">
<span className={`text-2xl font-black ${denetim.eksikler.length ? 'text-red-700' : 'text-emerald-700'}`}>%{denetim.uyum_yuzde}</span>
<span className="text-xs text-slate-500">{denetim.maddeler.length} gereklilik · {denetim.eksikler.length} eksik</span>
<span className={`text-2xl font-black ${denetim.eksikler.length || denetim.belirsizSayisi ? 'text-amber-700' : 'text-emerald-700'}`}>%{denetim.uyum_yuzde}</span>
<span className="text-xs text-slate-500">{denetim.maddeler.length} gereklilik · {denetim.eksikler.length} eksik · {denetim.belirsizSayisi ?? 0} kontrol gerekli</span>
</div>
<p className="text-xs text-slate-600">Listede alıntıyla desteklenen karşılanma oranı. {denetim.kapsam}</p>
{denetim.ozet && <p className="text-sm text-slate-700">{denetim.ozet}</p>}
<ul className="space-y-1.5">
{denetim.maddeler.map((m, i) => (
+25
View File
@@ -0,0 +1,25 @@
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {qualifyOfferAudit} from '../server/offer-audit.mjs';
const requirements=['Teslim süresi 30 gün','Numune sunulacak'];
const draft='Teslim süresi 30 gün olarak teklif edilmiştir.';
test('invented evidence and missing rows cannot count as satisfied or receive partial credit',()=>{
const out=qualifyOfferAudit({maddeler:[{gereklilik:requirements[0],durum:'karsilaniyor',kanit:'Ürünler 30 gün içinde teslim edilir.'}]},requirements,draft);
assert.equal(out.uyum_yuzde,0);assert.equal(out.belirsizSayisi,2);
assert.equal(out.maddeler[0].kanit,null);
});
test('reordered model rows match the actual requirement, not their array position',()=>{
const out=qualifyOfferAudit({maddeler:[{gereklilik:requirements[1],durum:'eksik'}, {gereklilik:requirements[0],durum:'karsilaniyor',kanit:'Teslim süresi 30 gün'}]},requirements,draft);
assert.equal(out.maddeler[0].durum,'karsilaniyor');assert.equal(out.maddeler[1].durum,'eksik');
assert.equal(out.uyum_yuzde,50);assert.deepEqual(out.eksikler,[requirements[1]]);
assert.match(out.kapsam,/tüm dokümanın/);
});
test('duplicate and mismatched requirement identities remain unresolved',()=>{
const row={gereklilik:requirements[0],durum:'karsilaniyor',kanit:'Teslim süresi 30 gün'};
const out=qualifyOfferAudit({maddeler:[row,row,{...row,gereklilik:'Değiştirilmiş gereklilik'}]},requirements,draft);
assert.equal(out.uyum_yuzde,0);assert.equal(out.belirsizSayisi,2);
});
test('blank and malformed model payloads fail instead of claiming a completed audit',()=>{
for(const result of [null,{}, {maddeler:[]},{maddeler:'wrong'}]) assert.throws(()=>qualifyOfferAudit(result,requirements,draft),/denetim maddelerini/);
const out=qualifyOfferAudit({maddeler:[null]},requirements,draft);assert.equal(out.belirsizSayisi,2);
});