Files
breakpilot-compliance/ai-compliance-sdk/internal/ucca/regulatory_news.go
T
Benjamin Admin 6b9c7984b4
CI / detect-changes (push) Successful in 8s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / build-sha-integrity (push) Successful in 7s
CI / validate-canonical-controls (push) Successful in 4s
CI / loc-budget (push) Successful in 18s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 3m2s
CI / test-go (push) Successful in 1m8s
CI / iace-gt-coverage (push) Successful in 19s
CI / test-python-backend (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
fix(ci): regulatory_news Zeitbomben-Test entschaerfen — test-go + Deploy entsperren
test-go failte seit 2026-06-19: VBR-OBL-001 ("Widerrufsbutton ab 19.06.2026") ist
seit dem Stichtag abgelaufen und faellt aus dem Zukunfts-Horizont von GetRegulatoryNews,
wodurch TestGetRegulatoryNews_FromRealFiles bricht. Fix: now-Referenz injizierbar
(GetRegulatoryNewsAt), Test nutzt fixes Datum -> deterministisch. Produktions-Caller
unveraendert (Wrapper). admin rag-query Marker, damit detect-changes admin mitbaut
(article_label-Rendering). go vet + alle ai-sdk-Tests lokal gruen.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-21 00:51:47 +02:00

153 lines
4.2 KiB
Go

package ucca
import (
"fmt"
"sort"
"strings"
"time"
)
// RegulatoryNewsItem is a single news item for dashboard display.
type RegulatoryNewsItem struct {
ID string `json:"id"`
Headline string `json:"headline"`
Summary string `json:"summary"`
LegalReference string `json:"legal_reference"`
Deadline string `json:"deadline"`
DaysRemaining int `json:"days_remaining"`
Urgency string `json:"urgency"` // critical, high, medium, low
Affected string `json:"affected"`
ActionRequired string `json:"action_required"`
ActionLink string `json:"action_link"`
Regulation string `json:"regulation"`
Sanctions string `json:"sanctions,omitempty"`
}
// RegulatoryNewsFilter controls which news items are returned.
type RegulatoryNewsFilter struct {
BusinessModel string `json:"business_model,omitempty"`
HorizonDays int `json:"horizon_days,omitempty"` // default 365
Limit int `json:"limit,omitempty"` // default 5
}
// GetRegulatoryNews scans all v2 obligations for upcoming deadlines
// and returns formatted news items sorted by urgency.
func GetRegulatoryNews(regulations map[string]*V2RegulationFile, filter RegulatoryNewsFilter) []RegulatoryNewsItem {
return GetRegulatoryNewsAt(regulations, filter, time.Now().UTC())
}
// GetRegulatoryNewsAt is GetRegulatoryNews with an injectable reference time so the
// upcoming-deadline window is deterministic in tests (no time-bomb once a deadline passes).
func GetRegulatoryNewsAt(regulations map[string]*V2RegulationFile, filter RegulatoryNewsFilter, now time.Time) []RegulatoryNewsItem {
if filter.HorizonDays <= 0 {
filter.HorizonDays = 365
}
if filter.Limit <= 0 {
filter.Limit = 5
}
today := now.UTC().Truncate(24 * time.Hour)
horizon := today.AddDate(0, 0, filter.HorizonDays)
var items []RegulatoryNewsItem
for _, reg := range regulations {
for _, obl := range reg.Obligations {
deadline, ok := resolveDeadline(obl)
if !ok || deadline.Before(today) || deadline.After(horizon) {
continue
}
days := int(deadline.Sub(today).Hours() / 24)
item := buildNewsItem(obl, reg.Regulation, deadline, days)
items = append(items, item)
}
}
sort.Slice(items, func(i, j int) bool {
return items[i].DaysRemaining < items[j].DaysRemaining
})
if len(items) > filter.Limit {
items = items[:filter.Limit]
}
return items
}
func buildNewsItem(obl V2Obligation, regulation string, deadline time.Time, days int) RegulatoryNewsItem {
item := RegulatoryNewsItem{
ID: obl.ID,
Deadline: deadline.Format("2006-01-02"),
DaysRemaining: days,
Urgency: computeUrgency(days),
Regulation: regulation,
}
// Use hand-crafted news if available
if obl.News != nil {
item.Headline = obl.News.Headline
item.Summary = obl.News.Summary
item.ActionRequired = obl.News.ActionRequired
item.Affected = obl.News.Affected
item.ActionLink = obl.News.ActionLink
} else {
// Auto-generate from obligation data
item.Headline = fmt.Sprintf("%s — ab %s", obl.Title, deadline.Format("02.01.2006"))
item.Summary = obl.Description
item.ActionRequired = "Pruefen Sie die Anforderungen und ergreifen Sie Massnahmen."
item.ActionLink = obl.BreakpilotFeature
}
item.LegalReference = formatLegalReference(obl.LegalBasis)
if obl.Sanctions != nil {
item.Sanctions = obl.Sanctions.MaxFine
}
return item
}
func resolveDeadline(obl V2Obligation) (time.Time, bool) {
// Check explicit deadline.date first
if obl.Deadline != nil && obl.Deadline.Date != "" {
t, err := time.Parse("2006-01-02", obl.Deadline.Date)
if err == nil {
return t, true
}
}
// Fallback to valid_from
if obl.ValidFrom != "" {
t, err := time.Parse("2006-01-02", obl.ValidFrom)
if err == nil {
return t, true
}
}
return time.Time{}, false
}
func computeUrgency(daysRemaining int) string {
switch {
case daysRemaining <= 30:
return "critical"
case daysRemaining <= 90:
return "high"
case daysRemaining <= 180:
return "medium"
default:
return "low"
}
}
func formatLegalReference(bases []V2LegalBasis) string {
if len(bases) == 0 {
return ""
}
var refs []string
for _, b := range bases {
ref := b.Article
if b.Norm != "" {
ref = b.Article + " " + b.Norm
}
refs = append(refs, ref)
}
return strings.Join(refs, ", ")
}