refactor(go): split obligations, portfolio, rbac, whistleblower handlers and stores, roadmap parser
Split 7 files exceeding the 500 LOC hard cap into 16 files, all under 500 LOC. No exported symbols renamed; zero behavior changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
236
ai-compliance-sdk/internal/roadmap/parser_row.go
Normal file
236
ai-compliance-sdk/internal/roadmap/parser_row.go
Normal file
@@ -0,0 +1,236 @@
|
||||
package roadmap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// parseRow parses a single row into a ParsedItem
|
||||
func (p *Parser) parseRow(row []string, columns []DetectedColumn, rowNum int) ParsedItem {
|
||||
item := ParsedItem{
|
||||
RowNumber: rowNum,
|
||||
IsValid: true,
|
||||
Data: RoadmapItemInput{},
|
||||
}
|
||||
|
||||
values := make(map[string]string)
|
||||
for i, col := range columns {
|
||||
if i < len(row) && col.MappedTo != "" {
|
||||
values[col.MappedTo] = strings.TrimSpace(row[i])
|
||||
}
|
||||
}
|
||||
|
||||
if title, ok := values["title"]; ok && title != "" {
|
||||
item.Data.Title = title
|
||||
} else {
|
||||
item.IsValid = false
|
||||
item.Errors = append(item.Errors, "Titel/Title ist erforderlich")
|
||||
}
|
||||
|
||||
if desc, ok := values["description"]; ok {
|
||||
item.Data.Description = desc
|
||||
}
|
||||
|
||||
if cat, ok := values["category"]; ok && cat != "" {
|
||||
item.Data.Category = p.parseCategory(cat)
|
||||
if item.Data.Category == "" {
|
||||
item.Warnings = append(item.Warnings, fmt.Sprintf("Unbekannte Kategorie: %s", cat))
|
||||
item.Data.Category = ItemCategoryTechnical
|
||||
}
|
||||
}
|
||||
|
||||
if prio, ok := values["priority"]; ok && prio != "" {
|
||||
item.Data.Priority = p.parsePriority(prio)
|
||||
if item.Data.Priority == "" {
|
||||
item.Warnings = append(item.Warnings, fmt.Sprintf("Unbekannte Priorität: %s", prio))
|
||||
item.Data.Priority = ItemPriorityMedium
|
||||
}
|
||||
}
|
||||
|
||||
if status, ok := values["status"]; ok && status != "" {
|
||||
item.Data.Status = p.parseStatus(status)
|
||||
if item.Data.Status == "" {
|
||||
item.Warnings = append(item.Warnings, fmt.Sprintf("Unbekannter Status: %s", status))
|
||||
item.Data.Status = ItemStatusPlanned
|
||||
}
|
||||
}
|
||||
|
||||
if ctrl, ok := values["control_id"]; ok {
|
||||
item.Data.ControlID = ctrl
|
||||
}
|
||||
|
||||
if reg, ok := values["regulation_ref"]; ok {
|
||||
item.Data.RegulationRef = reg
|
||||
}
|
||||
|
||||
if gap, ok := values["gap_id"]; ok {
|
||||
item.Data.GapID = gap
|
||||
}
|
||||
|
||||
if effort, ok := values["effort_days"]; ok && effort != "" {
|
||||
if days, err := strconv.Atoi(effort); err == nil {
|
||||
item.Data.EffortDays = &days
|
||||
}
|
||||
}
|
||||
|
||||
if assignee, ok := values["assignee"]; ok {
|
||||
item.Data.AssigneeName = assignee
|
||||
}
|
||||
|
||||
if dept, ok := values["department"]; ok {
|
||||
item.Data.Department = dept
|
||||
}
|
||||
|
||||
if startStr, ok := values["planned_start"]; ok && startStr != "" {
|
||||
if start := p.parseDate(startStr); start != nil {
|
||||
item.Data.PlannedStart = start
|
||||
}
|
||||
}
|
||||
|
||||
if endStr, ok := values["planned_end"]; ok && endStr != "" {
|
||||
if end := p.parseDate(endStr); end != nil {
|
||||
item.Data.PlannedEnd = end
|
||||
}
|
||||
}
|
||||
|
||||
if notes, ok := values["notes"]; ok {
|
||||
item.Data.Notes = notes
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
// parseCategory converts a string to ItemCategory
|
||||
func (p *Parser) parseCategory(s string) ItemCategory {
|
||||
s = strings.ToLower(strings.TrimSpace(s))
|
||||
|
||||
switch {
|
||||
case strings.Contains(s, "tech"):
|
||||
return ItemCategoryTechnical
|
||||
case strings.Contains(s, "org"):
|
||||
return ItemCategoryOrganizational
|
||||
case strings.Contains(s, "proz") || strings.Contains(s, "process"):
|
||||
return ItemCategoryProcessual
|
||||
case strings.Contains(s, "dok") || strings.Contains(s, "doc"):
|
||||
return ItemCategoryDocumentation
|
||||
case strings.Contains(s, "train") || strings.Contains(s, "schul"):
|
||||
return ItemCategoryTraining
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// parsePriority converts a string to ItemPriority
|
||||
func (p *Parser) parsePriority(s string) ItemPriority {
|
||||
s = strings.ToLower(strings.TrimSpace(s))
|
||||
|
||||
switch {
|
||||
case strings.Contains(s, "crit") || strings.Contains(s, "krit") || s == "1":
|
||||
return ItemPriorityCritical
|
||||
case strings.Contains(s, "high") || strings.Contains(s, "hoch") || s == "2":
|
||||
return ItemPriorityHigh
|
||||
case strings.Contains(s, "med") || strings.Contains(s, "mitt") || s == "3":
|
||||
return ItemPriorityMedium
|
||||
case strings.Contains(s, "low") || strings.Contains(s, "nied") || s == "4":
|
||||
return ItemPriorityLow
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// parseStatus converts a string to ItemStatus
|
||||
func (p *Parser) parseStatus(s string) ItemStatus {
|
||||
s = strings.ToLower(strings.TrimSpace(s))
|
||||
|
||||
switch {
|
||||
case strings.Contains(s, "plan") || strings.Contains(s, "offen") || strings.Contains(s, "open"):
|
||||
return ItemStatusPlanned
|
||||
case strings.Contains(s, "progress") || strings.Contains(s, "lauf") || strings.Contains(s, "arbeit"):
|
||||
return ItemStatusInProgress
|
||||
case strings.Contains(s, "block") || strings.Contains(s, "wart"):
|
||||
return ItemStatusBlocked
|
||||
case strings.Contains(s, "complet") || strings.Contains(s, "done") || strings.Contains(s, "fertig") || strings.Contains(s, "erledigt"):
|
||||
return ItemStatusCompleted
|
||||
case strings.Contains(s, "defer") || strings.Contains(s, "zurück") || strings.Contains(s, "verschob"):
|
||||
return ItemStatusDeferred
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// parseDate attempts to parse various date formats
|
||||
func (p *Parser) parseDate(s string) *time.Time {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
formats := []string{
|
||||
"2006-01-02",
|
||||
"02.01.2006",
|
||||
"2.1.2006",
|
||||
"02/01/2006",
|
||||
"2/1/2006",
|
||||
"01/02/2006",
|
||||
"1/2/2006",
|
||||
"2006/01/02",
|
||||
time.RFC3339,
|
||||
}
|
||||
|
||||
for _, format := range formats {
|
||||
if t, err := time.Parse(format, s); err == nil {
|
||||
return &t
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateAndEnrich validates parsed items and enriches them with mappings
|
||||
func (p *Parser) ValidateAndEnrich(items []ParsedItem, controls []string, regulations []string, gaps []string) []ParsedItem {
|
||||
controlSet := make(map[string]bool)
|
||||
for _, c := range controls {
|
||||
controlSet[strings.ToLower(c)] = true
|
||||
}
|
||||
|
||||
regSet := make(map[string]bool)
|
||||
for _, r := range regulations {
|
||||
regSet[strings.ToLower(r)] = true
|
||||
}
|
||||
|
||||
gapSet := make(map[string]bool)
|
||||
for _, g := range gaps {
|
||||
gapSet[strings.ToLower(g)] = true
|
||||
}
|
||||
|
||||
for i := range items {
|
||||
item := &items[i]
|
||||
|
||||
if item.Data.ControlID != "" {
|
||||
if controlSet[strings.ToLower(item.Data.ControlID)] {
|
||||
item.MatchedControl = item.Data.ControlID
|
||||
item.MatchConfidence = 1.0
|
||||
} else {
|
||||
item.Warnings = append(item.Warnings, fmt.Sprintf("Control '%s' nicht im Katalog gefunden", item.Data.ControlID))
|
||||
}
|
||||
}
|
||||
|
||||
if item.Data.RegulationRef != "" {
|
||||
if regSet[strings.ToLower(item.Data.RegulationRef)] {
|
||||
item.MatchedRegulation = item.Data.RegulationRef
|
||||
}
|
||||
}
|
||||
|
||||
if item.Data.GapID != "" {
|
||||
if gapSet[strings.ToLower(item.Data.GapID)] {
|
||||
item.MatchedGap = item.Data.GapID
|
||||
} else {
|
||||
item.Warnings = append(item.Warnings, fmt.Sprintf("Gap '%s' nicht im Mapping gefunden", item.Data.GapID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
Reference in New Issue
Block a user