package app import ( "fmt" "sort" "strings" "time" ) func detectTransportOwnerSwitchConflicts(current, next []TransportPolicyIntent) []TransportConflictRecord { curOwners := map[string]string{} for _, raw := range current { norm, key, _, err := normalizeTransportIntent(raw) if err != nil || key == "" || strings.TrimSpace(norm.ClientID) == "" { continue } curOwners[key] = norm.ClientID } if len(curOwners) == 0 { return nil } conflicts := make([]TransportConflictRecord, 0, 4) for _, raw := range next { norm, key, _, err := normalizeTransportIntent(raw) if err != nil || key == "" || strings.TrimSpace(norm.ClientID) == "" { continue } prevOwner, exists := curOwners[key] if !exists || prevOwner == norm.ClientID { continue } conflicts = append(conflicts, TransportConflictRecord{ Key: key, Type: "owner_switch", Severity: "block", Owners: []string{prevOwner, norm.ClientID}, Reason: fmt.Sprintf( "selector owner switch %s -> %s requires explicit override", prevOwner, norm.ClientID, ), SuggestedResolution: "use force_override + confirm token to switch owner", }) } return dedupeTransportConflicts(conflicts) } func transportOwnershipNeedsRebuild(policyRevision int64, owners TransportOwnershipState, planDigest string) bool { if owners.PolicyRevision != policyRevision { return true } expected := strings.TrimSpace(planDigest) if expected == "" { return false } if strings.TrimSpace(owners.PlanDigest) != expected { return true } return false } func buildTransportOwnershipStateFromPlan(plan TransportPolicyCompilePlan, policyRevision int64) TransportOwnershipState { now := time.Now().UTC().Format(time.RFC3339) items := make([]TransportOwnershipRecord, 0, plan.RuleCount) seen := map[string]struct{}{} for _, iface := range plan.Interfaces { for _, rule := range iface.Rules { key := strings.TrimSpace(rule.SelectorType) + ":" + strings.TrimSpace(rule.SelectorValue) if key == ":" { continue } if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} items = append(items, TransportOwnershipRecord{ Key: key, SelectorType: strings.TrimSpace(rule.SelectorType), SelectorValue: strings.TrimSpace(rule.SelectorValue), ClientID: strings.TrimSpace(rule.ClientID), ClientKind: strings.TrimSpace(rule.ClientKind), OwnerScope: strings.TrimSpace(rule.OwnerScope), IfaceID: strings.TrimSpace(iface.IfaceID), RoutingTable: strings.TrimSpace(iface.RoutingTable), MarkHex: strings.TrimSpace(rule.MarkHex), PriorityBase: rule.PriorityBase, Mode: strings.TrimSpace(rule.Mode), Priority: rule.Priority, UpdatedAt: now, }) } } sort.Slice(items, func(i, j int) bool { return items[i].Key < items[j].Key }) return TransportOwnershipState{ Version: transportStateVersion, UpdatedAt: now, PolicyRevision: policyRevision, PlanDigest: digestTransportPolicyCompilePlan(plan), Count: len(items), Items: items, } }