diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md index d472e68..28d14b4 100644 --- a/docs/quality/uygulama-durumu.md +++ b/docs/quality/uygulama-durumu.md @@ -365,3 +365,11 @@ Bu yalnız sunucu altyapısıdır. Metin düzenleme arayüzü, bu kaydın analiz İhale dokümanı “Metni incele” paneline DocumentTextReview eklendi. Kaynakla karşılaştırılmış düzeltme metni, sayfa/düzeltme açıklaması ve tüm sayfaları karşılaştırma beyanı alınır. Metin veya not değişince beyan sıfırlanır. Başarısız kayıt taslağı korur; sürüm uyuşmazlığında kullanıcıya taslağını kopyalayıp güncel kaydı yükleme seçeneği sunulur. Son kayıt sürümü/zamanı ve kaynak değişikliği açık gösterilir. Kayıt başarısı sunucu yanıtıyla gösterilir; bileşen kapanması/oturum değişiminden sonraki yanıtlar uygulanmaz. 79 test ve üretim derlemesi geçti. Formun gerçek tarayıcı kabulü ve kaydedilen incelemenin analizde sunucu tarafından çözülmesi henüz tamamlanmadı. Form, kayıt işleminin tek başına hazır teklif üretmediğini açıkça belirtiyor; mevcut OCR engeli devam ediyor. + +### Kaydedilmiş inceleme üzerinden analiz + +İnceleme formunda kaydedilmiş metin/not değişmemişse “Kaydedilmiş incelemeyi analiz et” sunuluyor. İstemci belge ID ve inceleme sürümünü gönderir; sunucu kaynak dosya özeti, sahiplik, İKN ve son inceleme sürümünü kontrol ederek metni DB'den alır. İstemcinin gönderdiği alternatif metin veya OCR onay bayrağı bu yolda kullanılmaz. AI işi bittikten sonra sürüm tekrar doğrulanır; arada değişiklik varsa sonuç reddedilir. Sonuçta sunucu kaynaklı inceleyen hesap, zaman, sürüm, dosya/metin özeti ve not tutulur. Bu yol OCR uyarısını kullanıcı incelemesiyle açıklayabilir; diğer analiz kusurları ve hazırlık kontrolleri aynen uygulanır. + +Hazırlık formu inceleme kaynağını gösterir. İnceleme sürümü kaynak snapshot'ına katıldı; aynı kalemlerle yeni inceleme sonucu geldiğinde eski kaynak kontrolü geçersizleşir. + +79 mevcut test ve ek snapshot testi geçti (80 ayrı test); üretim derlemesi geçti. Kaynak çözümleme testleri başka hesap/İKN, eski revizyon ve değişmiş dosyayı reddediyor. Gerçek AI sağlayıcısıyla tarayıcı kabulü ve çıktı anında sunucudan incelemenin hâlâ güncel olduğunun doğrulanması henüz açık. İnceleme kullanıcı beyanıdır, resmi doğruluk garantisi değildir. diff --git a/server/document-review.mjs b/server/document-review.mjs index 052470a..a46941c 100644 --- a/server/document-review.mjs +++ b/server/document-review.mjs @@ -41,3 +41,21 @@ export function saveDocumentReview(email,id,input) { } catch(error) {try {db.exec('ROLLBACK');} catch {} throw error;} finally {db.close();} } + +/** Resolve a requested review to server-owned text; caller text/approval is never used. */ +export function reviewedAnalysisSource(email,reference,tenderKey) { + if(!reference || typeof reference.documentId !== 'string' || !Number.isInteger(reference.revision)) + throw fail(400,'Geçerli belge ve inceleme sürümü gerekli.'); + const db=openDb(); + try { + const fileHash=source(db,email,reference.documentId); + const doc=db.prepare('SELECT ad,ikn FROM belgeler WHERE id=? AND email=?').get(reference.documentId,email); + if(!doc.ikn || doc.ikn!==tenderKey) throw fail(409,'İnceleme bu ihaleye bağlı değil.'); + const review=latest(db,email,reference.documentId); + if(!review || review.revision!==reference.revision || review.file_hash!==fileHash) + throw fail(409,'İnceleme veya kaynak dosya değişti; güncel incelemeyi açın.'); + return {text:review.corrected_text,fileName:doc.ad, + humanReview:{documentId:reference.documentId,revision:review.revision,fileHash,textHash:review.text_hash, + reviewedBy:email,reviewedAt:review.reviewed_at,note:review.review_note}}; + } finally {db.close();} +} diff --git a/server/index.js b/server/index.js index b31ac06..c671882 100644 --- a/server/index.js +++ b/server/index.js @@ -1,4 +1,4 @@ -import {documentReview,saveDocumentReview} from './document-review.mjs'; +import {documentReview,saveDocumentReview,reviewedAnalysisSource} from './document-review.mjs'; import { documentMetadataPatch, decodeDocumentUpload } from './document-metadata.mjs'; import { deadlineInfo } from '../src/services/tenderCalendar.js'; import { exchangeRates } from './exchange-rates.mjs'; @@ -1300,20 +1300,27 @@ const server = http.createServer(async (req, res) => { } try { const body = await readBody(req, 2 * 1024 * 1024); - if (!body.text || String(body.text).trim().length < 40) { + const reviewed=body.reviewedSource ? reviewedAnalysisSource(user.email,body.reviewedSource,body.tender?.ikn) : null; + const sourceText=reviewed ? reviewed.text : body.text; + if (!sourceText || String(sourceText).trim().length < 40) { return json(res, 400, { success: false, error: 'Analiz için yeterli metin yok (en az 40 karakter).' }); } const out = await analyzeSpec({ - text: body.text, - fileName: body.fileName, - extraction: body.extraction, + text: sourceText, + fileName: reviewed?.fileName || body.fileName, + extraction: reviewed ? {fileName:reviewed.fileName,complete:true,ocr:false} : body.extraction, tender: body.tender, chain: body.chain, mode: body.mode === 'derin' ? 'derin' : undefined, }); + if (reviewed) { + const latest=reviewedAnalysisSource(user.email,body.reviewedSource,body.tender?.ikn); + if(latest.humanReview.textHash!==reviewed.humanReview.textHash) throw Object.assign(new Error('İnceleme analiz sırasında değişti.'),{status:409}); + out.result.sourceReview={...out.result.sourceReview,humanReview:reviewed.humanReview}; + } return json(res, 200, { success: true, ...out, analyzedAt: new Date().toISOString() }); } catch (e) { - return json(res, 502, { success: false, error: e.message, attempts: e.attempts || null }); + return json(res, e.status || 502, { success: false, error: e.message, attempts: e.attempts || null }); } } diff --git a/src/components/BidPackageBuilder.jsx b/src/components/BidPackageBuilder.jsx index 7f3782d..9823fc5 100644 --- a/src/components/BidPackageBuilder.jsx +++ b/src/components/BidPackageBuilder.jsx @@ -245,6 +245,9 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => { {kaynakGecerlilik.status === 'conflict' &&
Kaynaklarda farklı süreler var. Güncel resmi dokümanı belirleyip analizi yenileyin; çelişki çözülmeden çıktı alınamaz.
} {[...new Set(kaynakGecerlilik.evidence.map(e=>e.quote))].map(quote=>{quote})} } + {effectiveResults.sourceReview?.humanReview &&
+ Metin incelemesi: sürüm {effectiveResults.sourceReview.humanReview.revision} · {effectiveResults.sourceReview.humanReview.reviewedBy} · {effectiveResults.sourceReview.humanReview.reviewedAt}. Kullanıcı karşılaştırmasıdır; resmi onay değildir. +
} {analizEksik &&Analiz kısmi: kaynak, OCR veya kalem bilgisi kontrol gerektiriyor. {(effectiveResults.sourceReview.issues || []).join(' ')} Tam ve doğrulanabilir kaynakla analizi tamamlayın.
} {/* Sizden beklenen bilgiler */} {eksikler.length > 0 && ( diff --git a/src/components/DocumentTextReview.jsx b/src/components/DocumentTextReview.jsx index 6927ed8..1f41cdf 100644 --- a/src/components/DocumentTextReview.jsx +++ b/src/components/DocumentTextReview.jsx @@ -2,7 +2,7 @@ import React,{useEffect,useRef,useState} from 'react'; import {getDocumentTextReview,saveDocumentTextReview} from '../services/account'; import {getToken} from '../services/auth'; -export function DocumentTextReview({documentId,extractedText}) { +export function DocumentTextReview({documentId,extractedText,onAnalyze}) { const [source,setSource]=useState(null); const [text,setText]=useState(extractedText || ''); const [note,setNote]=useState(''); @@ -10,6 +10,7 @@ export function DocumentTextReview({documentId,extractedText}) { const [busy,setBusy]=useState(false); const [error,setError]=useState(null); const [saved,setSaved]=useState(null); + const [analyzing,setAnalyzing]=useState(false); const live=useRef(true); const sequence=useRef(0); const load=async()=>{ @@ -38,27 +39,39 @@ export function DocumentTextReview({documentId,extractedText}) { } catch(e) {if(live.current && token===getToken())setError(e.message);} finally {if(live.current && token===getToken())setBusy(false);} }; + const analyze=async()=>{ + if(!source?.review || source.stale || busy || analyzing)return; + const token=getToken();setAnalyzing(true);setError(null); + try {await onAnalyze({documentId,revision:source.review.revision});} + catch(e) {if(live.current && token===getToken())setError(e.message);} + finally {if(live.current && token===getToken())setAnalyzing(false);} + }; returnBelgenin tüm sayfalarını açıp metin, miktar ve birimleri karşılaştırın. Bu kayıt kullanıcı incelemesidir; resmî onay veya e-imza değildir. Kaydetmek tek başına teklif dosyasını hazır yapmaz.
{source?.stale &&Kaynak dosya değişmiş. Önceki inceleme geçerli değil; tüm sayfaları yeniden karşılaştırın.
} {source?.review &&Son kayıt: sürüm {source.review.revision} · {new Date(source.review.reviewed_at).toLocaleString('tr-TR')}
} -{metinOnizleme.text || 'Okunabilir metin bulunamadı.'}
- {metinOnizleme.type === 'ihale' &&