Files
breakpilot-lehrer/admin-lehrer/lib/sdk/vendor-compliance/contract-review/analyzer.ts
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

460 lines
13 KiB
TypeScript

/**
* 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<ContractAnalysisResponse> {
// 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<string, unknown>,
request: ContractAnalysisRequest
): ContractAnalysisResponse {
const findings: Finding[] = (llmResponse.findings as Array<Record<string, unknown>> || []).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<Record<string, unknown>>) || []).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<string, unknown> || {}
return {
documentType: (llmResponse.document_type as DocumentType) || 'OTHER',
language: (llmResponse.language as 'de' | 'en') || 'de',
parties: ((llmResponse.parties as Array<Record<string, unknown>>) || []).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<Record<string, string>>) || []).map((r) => ({
de: r.de || '',
en: r.en || '',
})),
requiredActions: ((llmResponse.required_actions as Array<Record<string, string>>) || []).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}<mark data-citation-id="${citation.documentId}">${quoted}</mark>${after}`
}
return result
}
// ==========================================
// COMPLIANCE SCORE CALCULATION
// ==========================================
export interface ComplianceScoreBreakdown {
totalScore: number
categoryScores: Record<FindingCategory, number>
severityCounts: Record<FindingSeverity, number>
findingCounts: {
total: number
gaps: number
risks: number
ok: number
unknown: number
}
}
/**
* Calculate detailed compliance score
*/
export function calculateComplianceScore(findings: Finding[]): ComplianceScoreBreakdown {
const severityWeights: Record<FindingSeverity, number> = {
CRITICAL: 25,
HIGH: 15,
MEDIUM: 8,
LOW: 3,
}
const categoryWeights: Partial<Record<FindingCategory, number>> = {
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<Record<FindingCategory, number>> = {}
const severityCounts: Record<FindingSeverity, number> = {
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<FindingCategory, number>,
severityCounts,
findingCounts: {
total: findings.length,
gaps,
risks,
ok,
unknown,
},
}
}