Files
Sharang Parnerkar f7a5f9e1ed refactor(go/ucca): split license_policy, models, pdf_export, escalation_store, obligations_registry
Split 5 oversized files (501-583 LOC each) into focused units all under 500 LOC:
- license_policy.go → +_types.go (engine logic / type definitions)
- models.go → +_intake.go, +_assessment.go (enums+domains / intake structs / output+DB types)
- pdf_export.go → +_markdown.go (PDF export / markdown export)
- escalation_store.go → +_dsb.go (main escalation ops / DSB pool ops)
- obligations_registry.go → +_grouping.go (registry core / grouping methods)

All files remain in package ucca. Zero behavior changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 10:03:51 +02:00

489 lines
16 KiB
Go

package ucca
import (
"fmt"
"strings"
"time"
)
// =============================================================================
// License Policy Engine
// Handles license/copyright compliance for standards and norms
// =============================================================================
// 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
result.OutputRestrictions = &OutputRestrictions{
AllowQuotes: false,
MaxQuoteLength: 0,
RequireCitation: true,
AllowCopy: false,
AllowExport: false,
}
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
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
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"
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
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) {
if facts.Publisher == "DIN_MEDIA" {
if facts.AIUsePermitted != "YES" {
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) {
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
}
}
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
}
switch facts.OperationMode {
case "LINK_ONLY":
return false
case "NOTES_ONLY":
return false
case "EXCERPT_ONLY":
return false
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
}
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
}
// DecideIngest returns the ingest decision for a document
func (e *LicensePolicyEngine) DecideIngest(facts *LicensedContentFacts) *LicenseIngestDecision {
result := e.Evaluate(facts)
return &LicenseIngestDecision{
AllowMetadata: true, // Metadata is always allowed
AllowNotes: e.CanIngestNotes(facts),
AllowFulltext: e.CanIngestFulltext(facts),
Reason: result.Reason,
EffectiveMode: result.EffectiveMode,
}
}
// 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()
}