From 673d6c1bc83ef200572388aaa8852e15a3665991 Mon Sep 17 00:00:00 2001 From: ht Date: Sun, 6 Sep 2026 10:20:37 +0300 Subject: [PATCH] Audit all supplied requirements across complete long drafts --- docs/quality/uygulama-durumu.md | 12 +++++++++++ server/ai-teklif.mjs | 21 +++++++++++-------- server/offer-audit-parts.mjs | 34 +++++++++++++++++++++++++++++++ src/components/TeklifAsistani.jsx | 1 + src/services/offerResponse.js | 1 + tests/offer-audit-parts.test.mjs | 22 ++++++++++++++++++++ tests/offer-source.test.mjs | 7 +++++++ 7 files changed, 89 insertions(+), 9 deletions(-) create mode 100644 server/offer-audit-parts.mjs create mode 100644 tests/offer-audit-parts.test.mjs diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md index 1c9c782..acd8321 100644 --- a/docs/quality/uygulama-durumu.md +++ b/docs/quality/uygulama-durumu.md @@ -513,3 +513,15 @@ Sonuç: Tüm 108 test ve üretim derlemesi geçti. Büyük paket uyarıları sü Testler doğrudan `ihaleMetni`, `teklifDenetle` ve `teklifTaslak` üzerinden kısa 120 günlük kaynak ile farklı 60 günlük ilan önceliğini, boş/geçersiz metin reddini ve hata yutulmamasını doğrular; dış kaynak veya model çağrısı yapılmaz. Dosya seçici ve kaynak değiştirme tarayıcı kabulü açık. Tam kaynak sürümleme ve ortak analiz kapsamının tamamı kapanmış sayılmaz. Tüm 111 test ve üretim derlemesi geçti. Büyük paket uyarıları sürüyor. Canlı yayın yapılmadı. + +### D06 — Çok kalemli ve uzun taslak denetimi + +Denetim için ilk 30 ihale kalemi/ilk 60 gereklilik sınırı kaldırıldı; açıkça verilen koşullar ve ihale kalemleri tam listeye alınır. Taslağın 60.000 karakterde kesilmesi kaldırıldı. Denetim, gereklilik grupları ile taslak bölümlerinin tüm kombinasyonlarını en fazla iki eşzamanlı çağrıyla çalıştırır. Her çağrı 8.000 karakterlik gereklilik grubu ve 10.000 karakterlik taslak bölümü sınırlarıyla hazırlanır; genel şartname analizinin farklı çıktı şemasına sahip birleştiricisine gönderilmez. + +Her parçadaki karar gerçek taslak alıntısı ve gereklilik eşleşmesiyle kontrol edilir. Bir gereklilik ancak tüm taslak bölümleri başarıyla incelenmişse ve bir bölümde gerçek alıntıyla karşılanmışsa karşılanıyor sayılır. Bütün bölümlerde eksikse eksik; başarısız bölüm varsa veya hiçbir bölümde doğrulanmış kanıt yokken belirsiz karar varsa belirsizdir. Hiç bölüm denetlenemezse hata döner. Ekran gereklilik grubu, taslak bölümü ve başarısız kontrol sayısını gösterir. + +Sınırlar: Bölümler arasına yayılan kanıt insan kontrolü gerektirebilir. İlan metninden anahtar sözcükle gereklilik seçimi halen sınırlıdır; bu değişiklik tüm resmi şartların çıkarıldığını kanıtlamaz. Taslak üretiminin ayrı gereklilik sınırları bu turda değiştirilmedi. Uzun denetimin sunucuda kalıcı iş/iptal ve istemci zaman aşımı sonrası izlenmesi halen açık. Tek bir 8.000 karakteri aşan gereklilik kesilmez; kullanıcıdan bölmesi istenir. + +Testler: gerçek toplama fonksiyonunda 100 kalem + 80 açık koşul; denetim yürütücüsünde 100 gereklilik, 60.000 karakter sonrasındaki kanıt, bounded çağrı boyutu ve başarısız bölümün belirsizliğe dönüşmesi. Model yanıtları bu testte sentetiktir; gerçek sağlayıcı ve uzun tarayıcı kabulü değildir. + +Sonuç: Tüm 114 test ve üretim derlemesi geçti. Büyük paket uyarıları sürüyor. Planın tamamı bitmedi; canlı yayın yapılmadı. diff --git a/server/ai-teklif.mjs b/server/ai-teklif.mjs index 3f63182..2923639 100644 --- a/server/ai-teklif.mjs +++ b/server/ai-teklif.mjs @@ -1,7 +1,7 @@ +import { auditOfferInParts } from './offer-audit-parts.mjs'; import { qualifyOfferDraft } from './offer-draft.mjs'; import { createHash } from 'node:crypto'; import { questionChunks, qualifyQuestionAnswer } from './document-question.mjs'; -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: @@ -73,12 +73,12 @@ export async function belgeyeSor({ ikn, soru, text, tender, chain }) { kapsam:'Yanıt yalnız soruyla eşleşen kaynak parçalarına dayanır. Kaynak numarası kontrolü, yanıtın anlam bakımından doğruluğunu tek başına kanıtlamaz; diğer bölümlerde ek veya çelişen şartlar bulunabilir.'}; } -function gereklilikMetni(tender, gereklilikler, metin) { +export function gereklilikMetni(tender, gereklilikler, metin, allItems = false) { const g = []; for (const x of Array.isArray(gereklilikler) ? gereklilikler : []) if (x) g.push(typeof x === 'string' ? x : x.belge || x.item || x.ad || JSON.stringify(x)); for (const y of Array.isArray(tender?.yeterlikKriterleri) ? tender.yeterlikKriterleri : []) if (y) g.push(String(y)); - for (const i of Array.isArray(tender?.items) ? tender.items.slice(0, 30) : []) g.push(`Kalem: ${i.ad} ${i.miktar ?? ''} ${i.birim ?? ''}`.trim()); - if (tender?.ikn && kalemStore.available()) for (const k of ((kalemStore.kalemOku(tender.ikn) || {}).kalemler || []).slice(0, 30)) g.push(`Kalem: ${k.ad} ${k.miktar ?? ''} ${k.birim ?? ''}`.trim()); + for (const i of Array.isArray(tender?.items) ? tender.items.slice(0, allItems ? undefined : 30) : []) g.push(`Kalem: ${i.ad} ${i.miktar ?? ''} ${i.birim ?? ''}`.trim()); + if (tender?.ikn && kalemStore.available()) for (const k of ((kalemStore.kalemOku(tender.ikn) || {}).kalemler || []).slice(0, allItems ? undefined : 30)) g.push(`Kalem: ${k.ad} ${k.miktar ?? ''} ${k.birim ?? ''}`.trim()); if (metin) { // Önce anahtar sözcüğü geçen SATIR (madde) alınır; bütün metni yapıştırmak okunmaz oluyordu. const satirlar = String(metin).split(/\r?\n+/).map((x) => x.trim()).filter((x) => x.length > 15); @@ -89,18 +89,21 @@ function gereklilikMetni(tender, gereklilikler, metin) { if (p) g.push(`${s}: ${p.icerik.slice(0, 300).replace(/\s+/g, ' ')}`); } } - return [...new Set(g)].slice(0, 60); + return [...new Set(g)].slice(0, allItems ? undefined : 60); } export async function teklifDenetle({ ikn, taslak, gereklilikler, tender, chain, text }) { - const t = String(taslak ?? '').trim().slice(0, 60000); + const t = String(taslak ?? '').trim(); if (t.length < 200) throw new Error('Taslak en az 200 karakter olmalı.'); const { text: metin } = await ihaleMetni(ikn, text, tender?.aciklama); - const liste = gereklilikMetni(tender, gereklilikler, metin); + const liste = gereklilikMetni(tender, gereklilikler, metin, true); 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; 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' }); - return qualifyOfferAudit(out?.result, liste, t); + return auditOfferInParts(liste,t,async (group,draftPart) => { + const out = await analyzeSpec({text:`GEREKLİLİKLER:\n${group.map((g,i)=>`${i+1}. ${g}`).join('\n')}\n\nTEKLİF TASLAĞI BÖLÜMÜ:\n${draftPart}\n\nYalnızca JSON döndür.`, + tender:tender ? {...tender,items:[]} : undefined,chain,systemOverride:sistem,mode:'derin',_singlePart:true}); + return out?.result; + }); } const BOLUMLER = { diff --git a/server/offer-audit-parts.mjs b/server/offer-audit-parts.mjs new file mode 100644 index 0000000..a7f4f5c --- /dev/null +++ b/server/offer-audit-parts.mjs @@ -0,0 +1,34 @@ +import {splitAnalysisText} from './analysis-parts.mjs'; +import {qualifyOfferAudit} from './offer-audit.mjs'; + +/** Audit every supplied requirement against every draft part, without generic AI-schema merging. */ +export async function auditOfferInParts(requirements, draft, run) { + const groups=[];let current=[],size=0; + for(const requirement of requirements) { + if(requirement.length>8000) throw new Error('Tek bir denetim gerekliliği çok uzun. Kaynak maddeyi ayrı gerekliliklere ayırın.'); + if(current.length && size+requirement.length>8000){groups.push(current);current=[];size=0;} + current.push(requirement);size+=requirement.length+10; + } + if(current.length)groups.push(current); + const parts=splitAnalysisText(draft,10000),jobs=groups.flatMap((group,g)=>parts.map(part=>({group,g,part}))),results=[]; + for(let i=0;i{ + try { + const output=await run(job.group,job.part.text); + results.push({...job,review:qualifyOfferAudit(output,job.group,job.part.text)}); + } catch(error){results.push({...job,error:String(error.message || error)});} + })); + if(!results.some(result=>result.review))throw new Error('Taslağın hiçbir bölümü denetlenemedi. Yeniden deneyin.'); + const merged=[]; + for(const [g,group] of groups.entries()) for(const requirement of group) { + const matching=results.filter(result=>result.g===g); + const rows=matching.flatMap(result=>result.review?.maddeler.filter(row=>row.gereklilik===requirement) || []); + const verified=rows.find(row=>row.durum==='karsilaniyor'); + const failed=matching.some(result=>result.error); + merged.push(failed ? {gereklilik:requirement,durum:'belirsiz',oneri:'Taslağın bazı bölümleri denetlenemedi. Yeniden deneyin.'} + : verified || (rows.length===parts.length && rows.every(row=>row.durum==='eksik') + ? {gereklilik:requirement,durum:'eksik'} : {gereklilik:requirement,durum:'belirsiz'})); + } + const review=qualifyOfferAudit({maddeler:merged},requirements,draft); + return {...review,auditReview:{draftParts:parts.length,requirementGroups:groups.length,failedCalls:results.filter(row=>row.error).length}, + kapsam:review.kapsam+' Uzun taslak bölümler halinde kontrol edilir; bölümler arasına yayılan bir kanıt elle kontrol gerektirebilir.'}; +} diff --git a/src/components/TeklifAsistani.jsx b/src/components/TeklifAsistani.jsx index 28f7bbc..fee40d3 100644 --- a/src/components/TeklifAsistani.jsx +++ b/src/components/TeklifAsistani.jsx @@ -133,6 +133,7 @@ const OfferAssistantSession = ({ tender }) => { {denetim.maddeler.length} gereklilik · {denetim.eksikler.length} eksik · {denetim.belirsizSayisi ?? 0} kontrol gerekli

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

+ {denetim.auditReview &&

{denetim.auditReview.requirementGroups} gereklilik grubu · {denetim.auditReview.draftParts} taslak bölümü · {denetim.auditReview.failedCalls} başarısız kontrol

} {denetim.ozet &&

{denetim.ozet}

}