/** Lossless bounded chunks, including a short last clause and long unbroken lines. */ export function questionChunks(value, target = 1200) { if (!Number.isInteger(target) || target < 32 || target > 1400) throw new Error('Geçersiz kaynak parçası boyutu.'); const text = String(value ?? ''); const chunks = []; let offset = 0; while (offset < text.length) { let end = Math.min(offset + target, text.length); if (end < text.length) { const segment = text.slice(offset, end); const boundary = Math.max(segment.lastIndexOf('\n'), segment.lastIndexOf(' ')); if (boundary >= target / 2) end = offset + boundary + 1; // A Unicode surrogate pair is one character, never two source fragments. if (/[\uD800-\uDBFF]/.test(text[end - 1]) && /[\uDC00-\uDFFF]/.test(text[end])) end--; } chunks.push(text.slice(offset, end)); offset = end; } return chunks; } /** Reject fabricated references rather than attaching unrelated fallback sources. */ export function qualifyQuestionAnswer(result, selected) { if (typeof result?.cevap !== 'string' || !result.cevap.trim()) throw new Error('Model geçerli bir yanıt üretmedi. Yeniden deneyin.'); const cevap = result.cevap.trim(); const cited = [...new Set([...cevap.matchAll(/\[(\d+)\]/g)].map(match => Number(match[1])))]; if (!cited.length) throw new Error('Yanıt kaynak parçasına atıf içermiyor. Yeniden deneyin.'); const available = new Map(selected.map(part => [part.n, part])); if (cited.some(id => !available.has(id))) throw new Error('Yanıttaki kaynak numarası incelenen parçalarla uyuşmuyor. Yeniden deneyin.'); return {cevap, kaynaklar:cited.map(n => ({n, alinti:available.get(n).icerik}))}; }