/** * Contract Analyzer * * LLM-based contract review for GDPR compliance */ import { Finding, Citation, FindingType, FindingCategory, FindingSeverity, DocumentType, LocalizedText, } from '../types' import { AVV_CHECKLIST, INCIDENT_CHECKLIST, TRANSFER_CHECKLIST } from './checklists' // ========================================== // TYPES // ========================================== export interface ContractAnalysisRequest { contractId: string vendorId: string tenantId: string documentText: string documentType?: DocumentType language?: 'de' | 'en' analysisScope?: AnalysisScope[] } export interface ContractAnalysisResponse { documentType: DocumentType language: 'de' | 'en' parties: ContractPartyInfo[] findings: Finding[] complianceScore: number topRisks: LocalizedText[] requiredActions: LocalizedText[] metadata: ExtractedMetadata } export interface ContractPartyInfo { role: 'CONTROLLER' | 'PROCESSOR' | 'PARTY' name: string address?: string } export interface ExtractedMetadata { effectiveDate?: string expirationDate?: string autoRenewal?: boolean terminationNoticePeriod?: number governingLaw?: string jurisdiction?: string } export type AnalysisScope = | 'AVV_COMPLIANCE' | 'SUBPROCESSOR' | 'INCIDENT_RESPONSE' | 'AUDIT_RIGHTS' | 'DELETION' | 'TOM' | 'TRANSFER' | 'LIABILITY' | 'SLA' // ========================================== // SYSTEM PROMPTS // ========================================== export const CONTRACT_REVIEW_SYSTEM_PROMPT = `Du bist ein Datenschutz-Rechtsexperte, der Verträge auf DSGVO-Konformität prüft. WICHTIG: 1. Jede Feststellung MUSS mit einer Textstelle belegt werden (Citation) 2. Gib niemals Rechtsberatung - nur Compliance-Hinweise 3. Markiere unklare Stellen als UNKNOWN, nicht als GAP 4. Sei konservativ: im Zweifel RISK statt OK PRÜFUNGSSCHEMA Art. 28 DSGVO AVV: ${AVV_CHECKLIST.map((item) => `- ${item.id}: ${item.requirement.de} (${item.article})`).join('\n')} INCIDENT RESPONSE: ${INCIDENT_CHECKLIST.map((item) => `- ${item.id}: ${item.requirement.de} (${item.article})`).join('\n')} DRITTLANDTRANSFER: ${TRANSFER_CHECKLIST.map((item) => `- ${item.id}: ${item.requirement.de} (${item.article})`).join('\n')} AUSGABEFORMAT (JSON): { "document_type": "AVV|MSA|SLA|SCC|NDA|TOM_ANNEX|OTHER|UNKNOWN", "language": "de|en", "parties": [ { "role": "CONTROLLER|PROCESSOR|PARTY", "name": "...", "address": "..." } ], "findings": [ { "category": "AVV_CONTENT|SUBPROCESSOR|INCIDENT|AUDIT_RIGHTS|DELETION|TOM|TRANSFER|LIABILITY|SLA|DATA_SUBJECT_RIGHTS|CONFIDENTIALITY|INSTRUCTION|GENERAL", "type": "OK|GAP|RISK|UNKNOWN", "severity": "LOW|MEDIUM|HIGH|CRITICAL", "title_de": "...", "title_en": "...", "description_de": "...", "description_en": "...", "recommendation_de": "...", "recommendation_en": "...", "citations": [ { "page": 3, "quoted_text": "Der Auftragnehmer...", "start_char": 1234, "end_char": 1456 } ], "affected_requirement": "Art. 28 Abs. 3 lit. a DSGVO" } ], "compliance_score": 72, "top_risks": [ {"de": "...", "en": "..."} ], "required_actions": [ {"de": "...", "en": "..."} ], "metadata": { "effective_date": "2024-01-01", "expiration_date": "2025-12-31", "auto_renewal": true, "termination_notice_period": 90, "governing_law": "Germany", "jurisdiction": "Frankfurt am Main" } }` export const CONTRACT_CLASSIFICATION_PROMPT = `Analysiere den folgenden Vertragstext und klassifiziere ihn: 1. Dokumenttyp (AVV, MSA, SLA, SCC, NDA, TOM_ANNEX, OTHER) 2. Sprache (de, en) 3. Vertragsparteien mit Rollen Antworte im JSON-Format: { "document_type": "...", "language": "...", "parties": [...] }` export const METADATA_EXTRACTION_PROMPT = `Extrahiere die folgenden Metadaten aus dem Vertrag: 1. Inkrafttreten / Effective Date 2. Laufzeit / Ablaufdatum 3. Automatische Verlängerung 4. Kündigungsfrist 5. Anwendbares Recht 6. Gerichtsstand Antworte im JSON-Format.` // ========================================== // ANALYSIS FUNCTIONS // ========================================== /** * Analyze a contract for GDPR compliance */ export async function analyzeContract( request: ContractAnalysisRequest ): Promise { // This function would typically call an LLM API // For now, we provide the structure that would be used const apiEndpoint = '/api/sdk/v1/vendor-compliance/contracts/analyze' const response = await fetch(apiEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ contract_id: request.contractId, vendor_id: request.vendorId, tenant_id: request.tenantId, document_text: request.documentText, document_type: request.documentType, language: request.language || 'de', analysis_scope: request.analysisScope || [ 'AVV_COMPLIANCE', 'SUBPROCESSOR', 'INCIDENT_RESPONSE', 'AUDIT_RIGHTS', 'DELETION', 'TOM', 'TRANSFER', ], }), }) if (!response.ok) { throw new Error('Contract analysis failed') } const result = await response.json() return transformAnalysisResponse(result, request) } /** * Transform LLM response to typed response */ function transformAnalysisResponse( llmResponse: Record, request: ContractAnalysisRequest ): ContractAnalysisResponse { const findings: Finding[] = (llmResponse.findings as Array> || []).map((f, idx) => ({ id: `finding-${request.contractId}-${idx}`, tenantId: request.tenantId, contractId: request.contractId, vendorId: request.vendorId, type: (f.type as FindingType) || 'UNKNOWN', category: (f.category as FindingCategory) || 'GENERAL', severity: (f.severity as FindingSeverity) || 'MEDIUM', title: { de: (f.title_de as string) || '', en: (f.title_en as string) || '', }, description: { de: (f.description_de as string) || '', en: (f.description_en as string) || '', }, recommendation: f.recommendation_de ? { de: f.recommendation_de as string, en: (f.recommendation_en as string) || '', } : undefined, citations: ((f.citations as Array>) || []).map((c) => ({ documentId: request.contractId, page: (c.page as number) || 1, startChar: (c.start_char as number) || 0, endChar: (c.end_char as number) || 0, quotedText: (c.quoted_text as string) || '', quoteHash: generateQuoteHash((c.quoted_text as string) || ''), })), affectedRequirement: f.affected_requirement as string | undefined, triggeredControls: [], status: 'OPEN', createdAt: new Date(), updatedAt: new Date(), })) const metadata = llmResponse.metadata as Record || {} return { documentType: (llmResponse.document_type as DocumentType) || 'OTHER', language: (llmResponse.language as 'de' | 'en') || 'de', parties: ((llmResponse.parties as Array>) || []).map((p) => ({ role: (p.role as 'CONTROLLER' | 'PROCESSOR' | 'PARTY') || 'PARTY', name: (p.name as string) || '', address: p.address as string | undefined, })), findings, complianceScore: (llmResponse.compliance_score as number) || 0, topRisks: ((llmResponse.top_risks as Array>) || []).map((r) => ({ de: r.de || '', en: r.en || '', })), requiredActions: ((llmResponse.required_actions as Array>) || []).map((a) => ({ de: a.de || '', en: a.en || '', })), metadata: { effectiveDate: metadata.effective_date as string | undefined, expirationDate: metadata.expiration_date as string | undefined, autoRenewal: metadata.auto_renewal as boolean | undefined, terminationNoticePeriod: metadata.termination_notice_period as number | undefined, governingLaw: metadata.governing_law as string | undefined, jurisdiction: metadata.jurisdiction as string | undefined, }, } } /** * Generate a hash for quote verification */ function generateQuoteHash(text: string): string { // Simple hash for demo - in production use crypto.subtle.digest let hash = 0 for (let i = 0; i < text.length; i++) { const char = text.charCodeAt(i) hash = ((hash << 5) - hash) + char hash = hash & hash } return Math.abs(hash).toString(16).padStart(16, '0') } // ========================================== // CITATION UTILITIES // ========================================== /** * Verify citation integrity */ export function verifyCitation( citation: Citation, documentText: string ): boolean { const extractedText = documentText.substring(citation.startChar, citation.endChar) const expectedHash = generateQuoteHash(extractedText) return citation.quoteHash === expectedHash } /** * Find citation context in document */ export function getCitationContext( citation: Citation, documentText: string, contextChars: number = 100 ): { before: string quoted: string after: string } { const start = Math.max(0, citation.startChar - contextChars) const end = Math.min(documentText.length, citation.endChar + contextChars) return { before: documentText.substring(start, citation.startChar), quoted: documentText.substring(citation.startChar, citation.endChar), after: documentText.substring(citation.endChar, end), } } /** * Highlight citations in text */ export function highlightCitations( documentText: string, citations: Citation[] ): string { // Sort citations by start position (reverse to avoid offset issues) const sortedCitations = [...citations].sort((a, b) => b.startChar - a.startChar) let result = documentText for (const citation of sortedCitations) { const before = result.substring(0, citation.startChar) const quoted = result.substring(citation.startChar, citation.endChar) const after = result.substring(citation.endChar) result = `${before}${quoted}${after}` } return result } // ========================================== // COMPLIANCE SCORE CALCULATION // ========================================== export interface ComplianceScoreBreakdown { totalScore: number categoryScores: Record severityCounts: Record findingCounts: { total: number gaps: number risks: number ok: number unknown: number } } /** * Calculate detailed compliance score */ export function calculateComplianceScore(findings: Finding[]): ComplianceScoreBreakdown { const severityWeights: Record = { CRITICAL: 25, HIGH: 15, MEDIUM: 8, LOW: 3, } const categoryWeights: Partial> = { AVV_CONTENT: 1.5, SUBPROCESSOR: 1.3, INCIDENT: 1.3, DELETION: 1.2, AUDIT_RIGHTS: 1.1, TOM: 1.2, TRANSFER: 1.4, } let totalDeductions = 0 const maxPossibleDeductions = 100 const categoryScores: Partial> = {} const severityCounts: Record = { LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0, } let gaps = 0 let risks = 0 let ok = 0 let unknown = 0 for (const finding of findings) { severityCounts[finding.severity]++ switch (finding.type) { case 'GAP': gaps++ totalDeductions += severityWeights[finding.severity] * (categoryWeights[finding.category] || 1) break case 'RISK': risks++ totalDeductions += severityWeights[finding.severity] * 0.7 * (categoryWeights[finding.category] || 1) break case 'OK': ok++ break case 'UNKNOWN': unknown++ totalDeductions += severityWeights[finding.severity] * 0.3 * (categoryWeights[finding.category] || 1) break } } // Calculate category scores const categories = new Set(findings.map((f) => f.category)) for (const category of categories) { const categoryFindings = findings.filter((f) => f.category === category) const categoryOk = categoryFindings.filter((f) => f.type === 'OK').length const categoryTotal = categoryFindings.length categoryScores[category] = categoryTotal > 0 ? Math.round((categoryOk / categoryTotal) * 100) : 100 } const totalScore = Math.max(0, Math.round(100 - (totalDeductions / maxPossibleDeductions) * 100)) return { totalScore, categoryScores: categoryScores as Record, severityCounts, findingCounts: { total: findings.length, gaps, risks, ok, unknown, }, } }