fix(admin-v2): Restore complete admin-v2 application
The admin-v2 application was incomplete in the repository. This commit restores all missing components: - Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education, infrastructure, communication, development, onboarding, rbac - SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen, vendor-compliance, tom-generator, dsr, and more - Developer portal (25 pages): API docs, SDK guides, frameworks - All components, lib files, hooks, and types - Updated package.json with all dependencies The issue was caused by incomplete initial repository state - the full admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2 but was never fully synced to the main admin-v2 directory. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
459
admin-v2/lib/sdk/vendor-compliance/contract-review/analyzer.ts
Normal file
459
admin-v2/lib/sdk/vendor-compliance/contract-review/analyzer.ts
Normal file
@@ -0,0 +1,459 @@
|
||||
/**
|
||||
* 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user