feat: Dava Özeti, Çelişki Avcısı ve Dilekçe Taslağı'na mevzuat entegrasyonu

mevzuat-etl'in doldurduğu Supabase mevzuat/mevzuat_madde tablolarından
(aynı proje) ilgili kanun maddelerini bulup analiz/taslak üretiminden önce
prompt'a ekliyor. Bulma: önce dava metnindeki açık atıfları regex ile yakala
(TCK m.141, 5237 sayılı Kanun'un 141. maddesi gibi, tur-agnostik), boşsa
precedent.controller.ts'deki AI-keyword-sıkıştırma + tam-metin arama
desenine düşer. Hiçbir zaman exception fırlatmaz, mevzuat bulunamazsa analiz
normal devam eder.

Dilekçe Taslağı (generateDraft) farklı ele alınıyor — çıktısı JSON değil düz
metin ve aynen dilekçe belgesi olarak kullanılıyor, bu yüzden referans
bloğu ayrıca işaretlenip sistem promptuna "kopyalama, sadece atıf yap"
talimatı eklendi. generateAngarya (Süre Tutum/Mazeret/Vekaletname) bilerek
dışarıda bırakıldı — hukuki tartışma içermeyen idari dilekçeler.

Tasarım: mevzuat-etl/docs/superpowers/specs/2026-08-17-laawos-mevzuat-entegrasyonu-design.md

Not: drafts.used_legislation (jsonb) sütunu Supabase Studio'dan elle eklendi.
This commit is contained in:
Mustafa Yildiz
2026-08-17 00:46:26 +03:00
parent 66a3960d90
commit a5946eeb4a
3 changed files with 204 additions and 7 deletions
+21 -5
View File
@@ -3,6 +3,19 @@ import { supabase } from '../lib/supabase';
import { AuthenticatedRequest } from '../middleware/auth';
import { runIddianameAnalysis, runCaseNarrativeAnalysis, runStatementAnalysis, runDeficiencyAnalysis, runStrategyAnalysis, runMediationAnalysis, runDigitalInternQa } from './document.controller';
import { findOrCreateCaseByTitle } from '../lib/caseLookup';
import { findRelevantLegislation, LegislationMatch } from '../lib/legislationLookup';
function formatLegislationBlock(matches: LegislationMatch[]): string {
if (matches.length === 0) return '';
const body = matches
.map((m) => `${m.kanun} madde ${m.maddeNo}${m.baslik ? ` - ${m.baslik}` : ''}: ${m.metin}`)
.join('\n\n');
return `İLGİLİ MEVZUAT:\n${body}\n\n---\n\n`;
}
function toTraceableLegislation(matches: LegislationMatch[]) {
return matches.map(({ maddeNo, baslik, kanun }) => ({ maddeNo, baslik, kanun }));
}
export const createCase = async (req: AuthenticatedRequest, res: Response) => {
try {
@@ -89,15 +102,17 @@ export const summarizeCase = async (req: AuthenticatedRequest, res: Response) =>
.map((d) => `--- ${d.filename} ---\n${String(d.extracted_text).slice(0, perDocLimit)}`)
.join('\n\n');
const analysisInput = iddianameJson
const legislationMatches = await findRelevantLegislation(combinedText);
const analysisInput = formatLegislationBlock(legislationMatches) + (iddianameJson
? `İDDİANAME ÖN ANALİZİ (rapora bunu öncelikli bağlam olarak yansıt):\n${JSON.stringify(iddianameJson)}\n\n---\n\nDOSYADAKİ TÜM BELGELER:\n${combinedText}`
: combinedText;
: combinedText);
const { summaryJson, aiModel } = await runCaseNarrativeAnalysis(analysisInput);
const { data: analysisRecord, error: analysisError } = await supabase
.from('analyses')
.insert([{ case_id: caseId, document_id: null, summary_json: summaryJson, model_used: aiModel }])
.insert([{ case_id: caseId, document_id: null, summary_json: { ...summaryJson, kullanilan_mevzuat: toTraceableLegislation(legislationMatches) }, model_used: aiModel }])
.select()
.single();
if (analysisError) throw analysisError;
@@ -145,14 +160,15 @@ export const analyzeStatements = async (req: AuthenticatedRequest, res: Response
.map((d) => `--- ${d.filename} ---\n${String(d.extracted_text).slice(0, perDocLimit)}`)
.join('\n\n');
const analysisInput = `Aşağıdaki belgeler sadece ifade ve sorgu evraklarıdır:\n\n${combinedText}`;
const legislationMatches = await findRelevantLegislation(combinedText);
const analysisInput = formatLegislationBlock(legislationMatches) + `Aşağıdaki belgeler sadece ifade ve sorgu evraklarıdır:\n\n${combinedText}`;
const { summaryJson, aiModel } = await runStatementAnalysis(analysisInput);
// Çelişki analizi sonucunu 'analyses' tablosuna kaydet
const { data: analysisRecord, error: analysisError } = await supabase
.from('analyses')
.insert([{ case_id: caseId, document_id: null, summary_json: { statementAnalysis: summaryJson }, model_used: aiModel }])
.insert([{ case_id: caseId, document_id: null, summary_json: { statementAnalysis: summaryJson, kullanilan_mevzuat: toTraceableLegislation(legislationMatches) }, model_used: aiModel }])
.select()
.single();
if (analysisError) throw analysisError;
+24 -2
View File
@@ -2,6 +2,7 @@ import { Response } from 'express';
import { supabase } from '../lib/supabase';
import { AuthenticatedRequest } from '../middleware/auth';
import { callAiModelRaw } from './document.controller';
import { findRelevantLegislation, LegislationMatch } from '../lib/legislationLookup';
const DRAFT_SYSTEM_PROMPT = `
Sen uzman bir Türk avukatısın. Sana verilen dosya bilgilerine göre, istenen türde
@@ -14,6 +15,10 @@ const DRAFT_SYSTEM_PROMPT = `
ESKİ dosyaya ait somut bilgileri ASLA kullanma veya kopyalama — onların yerine SADECE
aşağıda verilen YENİ dosyanın gerçek bilgilerini koy.
EĞER sana "REFERANS MADDELER" verilmişse: bunları gerektiğinde "HUKUKİ SEBEPLER"
bölümünde doğal bir atıfla (örn. "TCK'nın 141. maddesi uyarınca...") kullan —
metinlerini olduğu gibi kopyalama, sadece atıf için referans al.
ÖNEMLİ — BİÇİMLENDİRME: Bu çıktı doğrudan bir dilekçe belgesine dönüştürülecek, o yüzden
KESİNLİKLE Markdown biçimlendirmesi kullanma (**, ##, -, *, 1. gibi işaretler YASAK).
Sadece düz metin yaz. Başlık/vurgu gerekiyorsa BÜYÜK HARF veya standart dilekçe
@@ -23,6 +28,18 @@ const DRAFT_SYSTEM_PROMPT = `
Sadece dilekçe taslağının kendisini döndür — başlık, açıklama veya ek yorum ekleme.
`;
function formatReferenceBlock(matches: LegislationMatch[]): string {
if (matches.length === 0) return '';
const body = matches
.map((m) => `${m.kanun} madde ${m.maddeNo}${m.baslik ? ` - ${m.baslik}` : ''}: ${m.metin}`)
.join('\n\n');
return `\n---\nREFERANS MADDELER (SADECE ATIF İÇİNDİR, DOĞRUDAN KOPYALAMA):\n${body}\n---\n`;
}
function toTraceableLegislation(matches: LegislationMatch[]) {
return matches.map(({ maddeNo, baslik, kanun }) => ({ maddeNo, baslik, kanun }));
}
// Dilekçe Hazırlama ekranındaki "AI ile Taslak Oluştur" — dosyanın gerçek bilgilerini
// (başlık, UYAP'tan gelen taraflar, varsa Dosya Özeti) ve kullanıcının notlarını,
// isteğe bağlı olarak seçilen büro şablonunun DİLİNİ/FORMATINI referans alarak
@@ -85,7 +102,11 @@ export const generateDraft = async (req: AuthenticatedRequest, res: Response) =>
userContent += `\n---\nBÜRO ŞABLONU "${template.name}" (SADECE dil/format/üslup referansı — içeriğini kopyalama):\n${template.extracted_text.slice(0, 6000)}\n`;
}
const { aiContent, aiModel } = await callAiModelRaw(DRAFT_SYSTEM_PROMPT, userContent, 2500);
const legislationLookupText = `${caseRow.title}\n${partiesText}\n${notes || ''}\n${typeof ozet === 'string' ? ozet : ''}`;
const legislationMatches = await findRelevantLegislation(legislationLookupText);
userContent += formatReferenceBlock(legislationMatches);
const { aiContent, aiModel } = await callAiModelRaw(DRAFT_SYSTEM_PROMPT, userContent, 2500, 180_000);
// Taslağı kalıcı olarak kaydediyoruz — hem "Dilekçelerim" listesinde hem ilgili
// dava dosyasının kendi "Dilekçeler" bölümünde görünsün, sayfadan çıkınca kaybolmasın.
@@ -97,6 +118,7 @@ export const generateDraft = async (req: AuthenticatedRequest, res: Response) =>
petition_type,
content: aiContent,
template_id: template_id || null,
used_legislation: toTraceableLegislation(legislationMatches),
}])
.select('id, created_at')
.single();
@@ -104,7 +126,7 @@ export const generateDraft = async (req: AuthenticatedRequest, res: Response) =>
console.error('Save Draft Error:', draftError);
}
res.json({ draft: aiContent, draftId: draftRecord?.id || null, model: aiModel, usedTemplate: !!template });
res.json({ draft: aiContent, draftId: draftRecord?.id || null, model: aiModel, usedTemplate: !!template, usedLegislation: toTraceableLegislation(legislationMatches) });
} catch (error: any) {
console.error('Generate Draft Error:', error);
res.status(500).json({ error: error.message || 'Internal Server Error' });
+159
View File
@@ -0,0 +1,159 @@
import { supabase } from './supabase';
import { callOllama } from './aiClient';
export interface LegislationMatch {
maddeNo: string;
baslik: string | null;
metin: string; // 800 karaktere kesilmiş
kanun: string; // mevzuat.kisa_ad || mevzuat.ad
}
const MAX_MADDE_LEN = 800;
const MAX_MATCHES = 5;
// mevzuat.tur sütunuyla eşleşen snake_case değerler — mevzuat-etl/mevzuat_etl/config.py::MEVZUAT_TUR_MAP
// ile aynı 8 anahtar, burada bağımsız bir TS kopyası (Python paketine bağımlılık yok).
const TUR_LABEL_TO_DB: Record<string, string> = {
'Kanun': 'kanun',
'KHK': 'khk',
'Tüzük': 'tuzuk',
'Cumhurbaşkanlığı Kararnamesi': 'cb_kararnamesi',
'Cumhurbaşkanı Kararı': 'cb_karari',
'Cumhurbaşkanlığı Yönetmeliği': 'cb_yonetmeligi',
'Tebliğ': 'teblig',
};
// Kısaltma bazlı, iki sıra da olası — "TCK m.141" (m./madde sayıdan ÖNCE) ve
// "CMK 100. maddesi" (sayı m./madde'den ÖNCE) — ayrı ayrı test edildi (bkz.
// _verify_legislation_lookup.ts), tek regex'te birleştirmek grup-indeksini
// karmaşıklaştırdığı için iki ayrı desen daha güvenli.
const ABBREVIATION_BEFORE_RE = /\b(TCK|CMK|TMK|TBK|TTK|ANAYASA)\b[^\d\n]{0,10}\b(?:m\.|madde)\s*(\d+[/A-Za-z]*)/gi;
const ABBREVIATION_AFTER_RE = /\b(TCK|CMK|TMK|TBK|TTK|ANAYASA)\b[^\d\n]{0,15}(\d+[/A-Za-z]*)\.?\s*madde/gi;
// Genel/tur-agnostik: "5237 sayılı Kanun'un 141. maddesi", "700 sayılı KHK'nın 3. maddesi".
const GENERIC_RE = /(\d+)\s+say[ıi]l[ıi]\s+(Kanun|KHK|Tüzük|Cumhurbaşkanlığı\s+Kararnamesi|Cumhurbaşkanı\s+Kararı|Cumhurbaşkanlığı\s+Yönetmeliği|Tebliğ)[^\d]{0,30}(\d+[/A-Za-z]*)\.?\s*madde/gi;
interface Citation {
kisaAd?: string;
mevzuatNo?: string;
turHint?: string; // DB'nin snake_case değeri
maddeNo: string;
}
function extractCitations(text: string): Citation[] {
const citations: Citation[] = [];
for (const m of text.matchAll(ABBREVIATION_BEFORE_RE)) {
citations.push({ kisaAd: m[1].toUpperCase(), maddeNo: m[2] });
}
for (const m of text.matchAll(ABBREVIATION_AFTER_RE)) {
citations.push({ kisaAd: m[1].toUpperCase(), maddeNo: m[2] });
}
for (const m of text.matchAll(GENERIC_RE)) {
citations.push({ mevzuatNo: m[1], turHint: TUR_LABEL_TO_DB[m[2]], maddeNo: m[3] });
}
// Tekilleştir (kisaAd|mevzuatNo + maddeNo bazında).
const seen = new Set<string>();
return citations.filter((c) => {
const key = `${c.kisaAd || c.mevzuatNo}:${c.maddeNo}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
function truncate(text: string): string {
if (text.length <= MAX_MADDE_LEN) return text;
return text.slice(0, MAX_MADDE_LEN) + '…';
}
async function resolveMevzuatId(citation: Citation): Promise<{ id: string; kanun: string } | null> {
let query = supabase.from('mevzuat').select('id, ad, kisa_ad');
if (citation.kisaAd) {
query = query.eq('kisa_ad', citation.kisaAd);
} else if (citation.mevzuatNo) {
query = query.eq('mevzuat_no', citation.mevzuatNo);
if (citation.turHint) query = query.eq('tur', citation.turHint);
} else {
return null;
}
const { data, error } = await query.maybeSingle();
if (error || !data) return null;
return { id: data.id, kanun: data.kisa_ad || data.ad };
}
async function fetchByCitations(citations: Citation[]): Promise<LegislationMatch[]> {
const matches: LegislationMatch[] = [];
for (const citation of citations) {
if (matches.length >= MAX_MATCHES) break;
const mevzuat = await resolveMevzuatId(citation);
if (!mevzuat) continue;
const { data, error } = await supabase
.from('mevzuat_madde')
.select('madde_no, baslik, metin')
.eq('mevzuat_id', mevzuat.id)
.eq('madde_no', citation.maddeNo)
.maybeSingle();
if (error || !data) continue;
matches.push({
maddeNo: data.madde_no,
baslik: data.baslik,
metin: truncate(data.metin),
kanun: mevzuat.kanun,
});
}
return matches;
}
const KEYWORD_PROMPT = `Sen uzman bir Türk avukatısın. Sana verilen dava metnini, ilgili kanun maddelerini bulmak için Türkçe tam-metin aramada kullanılacak 2 ila 4 temel hukuki terime çevir.
SADECE anahtar kelimeleri aralarına boşluk koyarak yaz. Noktalama işareti, açıklama veya ek kelime kullanma.`;
async function fetchByFullTextSearch(caseText: string): Promise<LegislationMatch[]> {
let keywords = '';
try {
const aiContent = await callOllama(
[
{ role: 'system', content: KEYWORD_PROMPT },
{ role: 'user', content: caseText.slice(0, 2000) },
],
{ numPredict: 40, timeoutMs: 20_000 }
);
keywords = (aiContent || '').trim().replace(/["'\n]/g, '');
} catch (err) {
console.error('Legislation keyword compression error:', err);
return [];
}
if (!keywords) return [];
const { data, error } = await supabase
.from('mevzuat_madde')
.select('madde_no, baslik, metin, mevzuat:mevzuat_id(ad, kisa_ad)')
.textSearch('search_vector', keywords, { type: 'websearch', config: 'turkish' })
.limit(MAX_MATCHES);
if (error || !data) return [];
return data.map((row: any) => ({
maddeNo: row.madde_no,
baslik: row.baslik,
metin: truncate(row.metin),
kanun: row.mevzuat?.kisa_ad || row.mevzuat?.ad || 'Bilinmeyen Mevzuat',
}));
}
// Dava metninde geçen kanun maddesi atıflarını bulur ve o maddelerin metnini döner.
// Hiçbir zaman exception fırlatmaz — Supabase/Ollama erişilemezse veya eşleşme
// yoksa boş dizi döner, çağıran taraf bunu "mevzuat bağlamı yok" olarak yorumlar.
export async function findRelevantLegislation(caseText: string): Promise<LegislationMatch[]> {
try {
const citations = extractCitations(caseText);
if (citations.length > 0) {
const matches = await fetchByCitations(citations);
if (matches.length > 0) return matches;
}
return await fetchByFullTextSearch(caseText);
} catch (err) {
console.error('findRelevantLegislation error:', err);
return [];
}
}