package ucca import ( "fmt" "strings" "time" ) // ============================================================================= // License Policy Engine // Handles license/copyright compliance for standards and norms // ============================================================================= // LicensedContentFacts represents the license-related facts from the wizard type LicensedContentFacts struct { Present bool `json:"present"` Publisher string `json:"publisher"` // DIN_MEDIA, VDI, VDE, ISO, etc. LicenseType string `json:"license_type"` // SINGLE_WORKSTATION, NETWORK_INTRANET, etc. AIUsePermitted string `json:"ai_use_permitted"` // YES, NO, UNKNOWN ProofUploaded bool `json:"proof_uploaded"` OperationMode string `json:"operation_mode"` // LINK_ONLY, NOTES_ONLY, FULLTEXT_RAG, TRAINING DistributionScope string `json:"distribution_scope"` // SINGLE_USER, COMPANY_INTERNAL, etc. ContentType string `json:"content_type"` // NORM_FULLTEXT, CUSTOMER_NOTES, etc. } // LicensePolicyResult represents the evaluation result type LicensePolicyResult struct { Allowed bool `json:"allowed"` EffectiveMode string `json:"effective_mode"` // The mode that will actually be used Reason string `json:"reason"` Gaps []LicenseGap `json:"gaps"` RequiredControls []LicenseControl `json:"required_controls"` StopLine *LicenseStopLine `json:"stop_line,omitempty"` // If hard blocked OutputRestrictions *OutputRestrictions `json:"output_restrictions"` EscalationLevel string `json:"escalation_level"` RiskScore int `json:"risk_score"` } // LicenseGap represents a license-related gap type LicenseGap struct { ID string `json:"id"` Title string `json:"title"` Description string `json:"description"` Controls []string `json:"controls"` Severity string `json:"severity"` } // LicenseControl represents a required control for license compliance type LicenseControl struct { ID string `json:"id"` Title string `json:"title"` Description string `json:"description"` WhatToDo string `json:"what_to_do"` Evidence []string `json:"evidence_needed"` } // LicenseStopLine represents a hard block type LicenseStopLine struct { ID string `json:"id"` Title string `json:"title"` Message string `json:"message"` Outcome string `json:"outcome"` // NOT_ALLOWED, NOT_ALLOWED_UNTIL_LICENSE_CLEARED } // OutputRestrictions defines how outputs should be filtered type OutputRestrictions struct { AllowQuotes bool `json:"allow_quotes"` MaxQuoteLength int `json:"max_quote_length"` // in characters RequireCitation bool `json:"require_citation"` AllowCopy bool `json:"allow_copy"` AllowExport bool `json:"allow_export"` } // LicensePolicyEngine evaluates license compliance type LicensePolicyEngine struct { // Configuration can be added here } // NewLicensePolicyEngine creates a new license policy engine func NewLicensePolicyEngine() *LicensePolicyEngine { return &LicensePolicyEngine{} } // Evaluate evaluates the license facts and returns the policy result func (e *LicensePolicyEngine) Evaluate(facts *LicensedContentFacts) *LicensePolicyResult { result := &LicensePolicyResult{ Allowed: true, EffectiveMode: "LINK_ONLY", // Default safe mode Gaps: []LicenseGap{}, RequiredControls: []LicenseControl{}, OutputRestrictions: &OutputRestrictions{ AllowQuotes: false, MaxQuoteLength: 0, RequireCitation: true, AllowCopy: false, AllowExport: false, }, RiskScore: 0, } // If no licensed content, return early with no restrictions if !facts.Present { result.EffectiveMode = "UNRESTRICTED" result.OutputRestrictions = &OutputRestrictions{ AllowQuotes: true, MaxQuoteLength: -1, // unlimited RequireCitation: false, AllowCopy: true, AllowExport: true, } return result } // Evaluate based on operation mode and license status switch facts.OperationMode { case "LINK_ONLY": e.evaluateLinkOnlyMode(facts, result) case "NOTES_ONLY": e.evaluateNotesOnlyMode(facts, result) case "EXCERPT_ONLY": e.evaluateExcerptOnlyMode(facts, result) case "FULLTEXT_RAG": e.evaluateFulltextRAGMode(facts, result) case "TRAINING": e.evaluateTrainingMode(facts, result) default: // Unknown mode, default to LINK_ONLY e.evaluateLinkOnlyMode(facts, result) } // Check publisher-specific restrictions e.applyPublisherRestrictions(facts, result) // Check distribution scope vs license type e.checkDistributionScope(facts, result) return result } // evaluateLinkOnlyMode - safest mode, always allowed func (e *LicensePolicyEngine) evaluateLinkOnlyMode(facts *LicensedContentFacts, result *LicensePolicyResult) { result.EffectiveMode = "LINK_ONLY" result.Allowed = true result.Reason = "Link-only Modus ist ohne spezielle Lizenz erlaubt" result.RiskScore = 0 // Very restrictive output result.OutputRestrictions = &OutputRestrictions{ AllowQuotes: false, MaxQuoteLength: 0, RequireCitation: true, AllowCopy: false, AllowExport: false, } // Recommend control for proper setup result.RequiredControls = append(result.RequiredControls, LicenseControl{ ID: "CTRL-LINK-ONLY-MODE", Title: "Link-only / Evidence Navigator aktivieren", Description: "Nur Verweise und Checklisten, kein Volltext", WhatToDo: "System auf LINK_ONLY konfigurieren, keine Normtexte indexieren", Evidence: []string{"System-Konfiguration", "Stichproben-Audit"}, }) } // evaluateNotesOnlyMode - customer notes, usually allowed func (e *LicensePolicyEngine) evaluateNotesOnlyMode(facts *LicensedContentFacts, result *LicensePolicyResult) { result.EffectiveMode = "NOTES_ONLY" result.Allowed = true result.Reason = "Notes-only Modus mit kundeneigenen Zusammenfassungen" result.RiskScore = 10 // Allow paraphrased content result.OutputRestrictions = &OutputRestrictions{ AllowQuotes: false, // No direct quotes from norms MaxQuoteLength: 0, RequireCitation: true, AllowCopy: true, // Can copy own notes AllowExport: true, // Can export own notes } result.RequiredControls = append(result.RequiredControls, LicenseControl{ ID: "CTRL-NOTES-ONLY-RAG", Title: "Notes-only RAG (kundeneigene Paraphrasen)", Description: "Nur kundeneigene Zusammenfassungen indexieren", WhatToDo: "UI-Flow fuer Notes-Erstellung, kein Copy/Paste von Originaltexten", Evidence: []string{"Notes-Provenance-Log", "Stichproben"}, }) // Add gap if license type is unknown if facts.LicenseType == "UNKNOWN" { result.Gaps = append(result.Gaps, LicenseGap{ ID: "GAP_LICENSE_UNKNOWN", Title: "Lizenzlage unklar", Description: "Die Lizenzlage sollte geklaert werden", Controls: []string{"CTRL-LICENSE-PROOF"}, Severity: "WARN", }) result.EscalationLevel = "E2" } } // evaluateExcerptOnlyMode - short quotes under citation rights func (e *LicensePolicyEngine) evaluateExcerptOnlyMode(facts *LicensedContentFacts, result *LicensePolicyResult) { result.EffectiveMode = "EXCERPT_ONLY" result.RiskScore = 30 // Check if AI use is permitted if facts.AIUsePermitted == "NO" || facts.AIUsePermitted == "UNKNOWN" { // Downgrade to NOTES_ONLY result.EffectiveMode = "NOTES_ONLY" result.Reason = "Excerpt-Modus nicht erlaubt ohne AI-Freigabe, Downgrade auf Notes-only" result.Gaps = append(result.Gaps, LicenseGap{ ID: "GAP_AI_USE_NOT_PERMITTED", Title: "AI/TDM-Nutzung nicht erlaubt", Description: "Zitate erfordern AI-Nutzungserlaubnis", Controls: []string{"CTRL-LICENSE-PROOF", "CTRL-NOTES-ONLY-RAG"}, Severity: "WARN", }) result.EscalationLevel = "E2" } else { result.Allowed = true result.Reason = "Kurze Zitate im Rahmen des Zitatrechts" } result.OutputRestrictions = &OutputRestrictions{ AllowQuotes: facts.AIUsePermitted == "YES", MaxQuoteLength: 150, // Max 150 characters per quote RequireCitation: true, AllowCopy: false, AllowExport: false, } result.RequiredControls = append(result.RequiredControls, LicenseControl{ ID: "CTRL-OUTPUT-GUARD-QUOTES", Title: "Output-Guard: Quote-Limits", Description: "Zitatlänge begrenzen", WhatToDo: "Max. 150 Zeichen pro Zitat, immer mit Quellenangabe", Evidence: []string{"Output-Guard-Konfiguration"}, }) } // evaluateFulltextRAGMode - requires explicit license proof func (e *LicensePolicyEngine) evaluateFulltextRAGMode(facts *LicensedContentFacts, result *LicensePolicyResult) { result.RiskScore = 60 // Check if AI use is explicitly permitted AND proof is uploaded if facts.AIUsePermitted == "YES" && facts.ProofUploaded { result.EffectiveMode = "FULLTEXT_RAG" result.Allowed = true result.Reason = "Volltext-RAG mit nachgewiesener AI-Lizenz" result.OutputRestrictions = &OutputRestrictions{ AllowQuotes: true, MaxQuoteLength: 500, // Still limited RequireCitation: true, AllowCopy: false, // No copy to prevent redistribution AllowExport: false, // No export } result.RequiredControls = append(result.RequiredControls, LicenseControl{ ID: "CTRL-LICENSE-GATED-INGEST", Title: "License-gated Ingest", Description: "Technische Durchsetzung der Lizenzpruefung", WhatToDo: "Ingest-Pipeline prueft Lizenz vor Indexierung", Evidence: []string{"Ingest-Audit-Logs"}, }, LicenseControl{ ID: "CTRL-TENANT-ISOLATION-STANDARDS", Title: "Tenant-Isolation", Description: "Strikte Trennung lizenzierter Inhalte", WhatToDo: "Keine Cross-Tenant-Suche, kein Export", Evidence: []string{"Tenant-Isolation-Dokumentation"}, }, ) } else { // NOT ALLOWED - downgrade to LINK_ONLY result.Allowed = false result.EffectiveMode = "LINK_ONLY" result.Reason = "Volltext-RAG ohne Lizenznachweis nicht erlaubt" result.Gaps = append(result.Gaps, LicenseGap{ ID: "GAP_FULLTEXT_WITHOUT_PROOF", Title: "Volltext-RAG ohne Lizenznachweis", Description: "Volltext-RAG erfordert nachgewiesene AI-Nutzungserlaubnis", Controls: []string{"CTRL-LICENSE-PROOF", "CTRL-LINK-ONLY-MODE"}, Severity: "BLOCK", }) result.EscalationLevel = "E3" // Set stop line result.StopLine = &LicenseStopLine{ ID: "STOP_FULLTEXT_WITHOUT_PROOF", Title: "Volltext-RAG blockiert", Message: "Volltext-RAG erfordert einen Nachweis der AI-Nutzungserlaubnis. Bitte laden Sie den Lizenzvertrag hoch oder wechseln Sie auf Link-only Modus.", Outcome: "NOT_ALLOWED_UNTIL_LICENSE_CLEARED", } result.OutputRestrictions = &OutputRestrictions{ AllowQuotes: false, MaxQuoteLength: 0, RequireCitation: true, AllowCopy: false, AllowExport: false, } } } // evaluateTrainingMode - most restrictive, rarely allowed func (e *LicensePolicyEngine) evaluateTrainingMode(facts *LicensedContentFacts, result *LicensePolicyResult) { result.RiskScore = 80 // Training is almost always blocked for standards if facts.AIUsePermitted == "YES" && facts.ProofUploaded && facts.LicenseType == "AI_LICENSE" { result.EffectiveMode = "TRAINING" result.Allowed = true result.Reason = "Training mit expliziter AI-Training-Lizenz" result.EscalationLevel = "E3" // Still requires review } else { // HARD BLOCK result.Allowed = false result.EffectiveMode = "LINK_ONLY" result.Reason = "Training auf Standards ohne explizite AI-Training-Lizenz verboten" result.StopLine = &LicenseStopLine{ ID: "STOP_TRAINING_WITHOUT_PROOF", Title: "Training blockiert", Message: "Modell-Training mit lizenzierten Standards ist ohne explizite AI-Training-Lizenz nicht zulaessig. DIN Media hat dies ausdruecklich ausgeschlossen.", Outcome: "NOT_ALLOWED", } result.Gaps = append(result.Gaps, LicenseGap{ ID: "GAP_TRAINING_ON_STANDARDS", Title: "Training auf Standards verboten", Description: "Modell-Training erfordert explizite AI-Training-Lizenz", Controls: []string{"CTRL-LICENSE-PROOF"}, Severity: "BLOCK", }) result.EscalationLevel = "E3" } result.OutputRestrictions = &OutputRestrictions{ AllowQuotes: false, MaxQuoteLength: 0, RequireCitation: true, AllowCopy: false, AllowExport: false, } } // applyPublisherRestrictions applies publisher-specific rules func (e *LicensePolicyEngine) applyPublisherRestrictions(facts *LicensedContentFacts, result *LicensePolicyResult) { // DIN Media specific restrictions if facts.Publisher == "DIN_MEDIA" { if facts.AIUsePermitted != "YES" { // DIN Media explicitly prohibits AI use without license if facts.OperationMode == "FULLTEXT_RAG" || facts.OperationMode == "TRAINING" { result.Allowed = false result.EffectiveMode = "LINK_ONLY" result.StopLine = &LicenseStopLine{ ID: "STOP_DIN_FULLTEXT_AI_NOT_ALLOWED", Title: "DIN Media AI-Nutzung blockiert", Message: "DIN Media untersagt die AI-Nutzung von Normen ohne explizite Genehmigung. Ein AI-Lizenzmodell ist erst ab Q4/2025 geplant. Bitte nutzen Sie Link-only oder Notes-only Modus.", Outcome: "NOT_ALLOWED_UNTIL_LICENSE_CLEARED", } result.Gaps = append(result.Gaps, LicenseGap{ ID: "GAP_DIN_MEDIA_WITHOUT_AI_LICENSE", Title: "DIN Media ohne AI-Lizenz", Description: "DIN Media verbietet AI-Nutzung ohne explizite Genehmigung", Controls: []string{"CTRL-LINK-ONLY-MODE", "CTRL-NO-CRAWLING-DIN"}, Severity: "BLOCK", }) result.EscalationLevel = "E3" result.RiskScore = 70 } } // Always add no-crawling control for DIN Media result.RequiredControls = append(result.RequiredControls, LicenseControl{ ID: "CTRL-NO-CRAWLING-DIN", Title: "Crawler-Block fuer DIN Media", Description: "Keine automatisierten Abrufe von DIN-Normen-Portalen", WhatToDo: "Domain-Denylist konfigurieren, nur manueller Import", Evidence: []string{"Domain-Denylist", "Fetch-Logs"}, }) } } // checkDistributionScope checks if distribution scope matches license type func (e *LicensePolicyEngine) checkDistributionScope(facts *LicensedContentFacts, result *LicensePolicyResult) { // Single workstation license with broad distribution if facts.LicenseType == "SINGLE_WORKSTATION" { if facts.DistributionScope == "COMPANY_INTERNAL" || facts.DistributionScope == "SUBSIDIARIES" || facts.DistributionScope == "EXTERNAL_CUSTOMERS" { result.Gaps = append(result.Gaps, LicenseGap{ ID: "GAP_DISTRIBUTION_SCOPE_MISMATCH", Title: "Verteilungsumfang uebersteigt Lizenz", Description: "Einzelplatz-Lizenz erlaubt keine unternehmensweite Nutzung", Controls: []string{"CTRL-LICENSE-PROOF", "CTRL-LINK-ONLY-MODE"}, Severity: "WARN", }) result.EscalationLevel = "E3" result.RiskScore += 20 } } // Network license with external distribution if facts.LicenseType == "NETWORK_INTRANET" { if facts.DistributionScope == "EXTERNAL_CUSTOMERS" { result.Gaps = append(result.Gaps, LicenseGap{ ID: "GAP_DISTRIBUTION_SCOPE_EXTERNAL", Title: "Externe Verteilung mit Intranet-Lizenz", Description: "Intranet-Lizenz erlaubt keine externe Verteilung", Controls: []string{"CTRL-LICENSE-PROOF"}, Severity: "WARN", }) result.EscalationLevel = "E2" result.RiskScore += 15 } } } // CanIngestFulltext checks if fulltext ingestion is allowed func (e *LicensePolicyEngine) CanIngestFulltext(facts *LicensedContentFacts) bool { if !facts.Present { return true // No licensed content, no restrictions } switch facts.OperationMode { case "LINK_ONLY": return false // Only metadata/references case "NOTES_ONLY": return false // Only customer notes, not fulltext case "EXCERPT_ONLY": return false // Only short excerpts case "FULLTEXT_RAG": return facts.AIUsePermitted == "YES" && facts.ProofUploaded case "TRAINING": return facts.AIUsePermitted == "YES" && facts.ProofUploaded && facts.LicenseType == "AI_LICENSE" default: return false } } // CanIngestNotes checks if customer notes can be ingested func (e *LicensePolicyEngine) CanIngestNotes(facts *LicensedContentFacts) bool { if !facts.Present { return true } // Notes are allowed in most modes return facts.OperationMode == "NOTES_ONLY" || facts.OperationMode == "EXCERPT_ONLY" || facts.OperationMode == "FULLTEXT_RAG" || facts.OperationMode == "TRAINING" } // GetEffectiveMode returns the effective operation mode after policy evaluation func (e *LicensePolicyEngine) GetEffectiveMode(facts *LicensedContentFacts) string { result := e.Evaluate(facts) return result.EffectiveMode } // LicenseIngestDecision represents the decision for ingesting a document type LicenseIngestDecision struct { AllowFulltext bool `json:"allow_fulltext"` AllowNotes bool `json:"allow_notes"` AllowMetadata bool `json:"allow_metadata"` Reason string `json:"reason"` EffectiveMode string `json:"effective_mode"` } // DecideIngest returns the ingest decision for a document func (e *LicensePolicyEngine) DecideIngest(facts *LicensedContentFacts) *LicenseIngestDecision { result := e.Evaluate(facts) decision := &LicenseIngestDecision{ AllowMetadata: true, // Metadata is always allowed AllowNotes: e.CanIngestNotes(facts), AllowFulltext: e.CanIngestFulltext(facts), Reason: result.Reason, EffectiveMode: result.EffectiveMode, } return decision } // LicenseAuditEntry represents an audit log entry for license decisions type LicenseAuditEntry struct { Timestamp time.Time `json:"timestamp"` TenantID string `json:"tenant_id"` DocumentID string `json:"document_id,omitempty"` Facts *LicensedContentFacts `json:"facts"` Decision string `json:"decision"` // ALLOW, DENY, DOWNGRADE EffectiveMode string `json:"effective_mode"` Reason string `json:"reason"` StopLineID string `json:"stop_line_id,omitempty"` } // FormatAuditEntry creates an audit entry for logging func (e *LicensePolicyEngine) FormatAuditEntry(tenantID string, documentID string, facts *LicensedContentFacts, result *LicensePolicyResult) *LicenseAuditEntry { decision := "ALLOW" if !result.Allowed { decision = "DENY" } else if result.EffectiveMode != facts.OperationMode { decision = "DOWNGRADE" } entry := &LicenseAuditEntry{ Timestamp: time.Now().UTC(), TenantID: tenantID, DocumentID: documentID, Facts: facts, Decision: decision, EffectiveMode: result.EffectiveMode, Reason: result.Reason, } if result.StopLine != nil { entry.StopLineID = result.StopLine.ID } return entry } // FormatHumanReadableSummary creates a human-readable summary of the evaluation func (e *LicensePolicyEngine) FormatHumanReadableSummary(result *LicensePolicyResult) string { var sb strings.Builder sb.WriteString("=== Lizenz-Policy Bewertung ===\n\n") if result.Allowed { sb.WriteString(fmt.Sprintf("Status: ERLAUBT\n")) } else { sb.WriteString(fmt.Sprintf("Status: BLOCKIERT\n")) } sb.WriteString(fmt.Sprintf("Effektiver Modus: %s\n", result.EffectiveMode)) sb.WriteString(fmt.Sprintf("Risiko-Score: %d\n", result.RiskScore)) sb.WriteString(fmt.Sprintf("Begruendung: %s\n\n", result.Reason)) if result.StopLine != nil { sb.WriteString("!!! STOP-LINE !!!\n") sb.WriteString(fmt.Sprintf(" %s: %s\n", result.StopLine.ID, result.StopLine.Title)) sb.WriteString(fmt.Sprintf(" %s\n\n", result.StopLine.Message)) } if len(result.Gaps) > 0 { sb.WriteString("Identifizierte Luecken:\n") for _, gap := range result.Gaps { sb.WriteString(fmt.Sprintf(" - [%s] %s: %s\n", gap.Severity, gap.ID, gap.Title)) } sb.WriteString("\n") } if len(result.RequiredControls) > 0 { sb.WriteString("Erforderliche Massnahmen:\n") for _, ctrl := range result.RequiredControls { sb.WriteString(fmt.Sprintf(" - %s: %s\n", ctrl.ID, ctrl.Title)) } sb.WriteString("\n") } if result.OutputRestrictions != nil { sb.WriteString("Output-Einschraenkungen:\n") sb.WriteString(fmt.Sprintf(" Zitate erlaubt: %v\n", result.OutputRestrictions.AllowQuotes)) sb.WriteString(fmt.Sprintf(" Max. Zitatlaenge: %d Zeichen\n", result.OutputRestrictions.MaxQuoteLength)) sb.WriteString(fmt.Sprintf(" Copy erlaubt: %v\n", result.OutputRestrictions.AllowCopy)) sb.WriteString(fmt.Sprintf(" Export erlaubt: %v\n", result.OutputRestrictions.AllowExport)) } return sb.String() }