package service import ( "errors" "fmt" "sort" ) type DependencySuppressionInput struct { DependencyID string `json:"dependencyId"` ServiceID string `json:"serviceId"` DependsOnServiceID string `json:"dependsOnServiceId"` SourceID string `json:"sourceId,omitempty"` Confidence float64 `json:"confidence"` Confirmed bool `json:"confirmed"` UpstreamState string `json:"upstreamState"` Suppress bool `json:"suppress"` Reason string `json:"reason"` } // ValidateDependencyGraph rejects dependency cycles deterministically. Only // depends_on edges participate in propagation; backs/exposes remain visible // topology links without changing service state. func ValidateDependencyGraph(dependencies []Dependency) error { edges := append([]Dependency(nil), dependencies...) SortDependencies(edges) seen := make(map[string]struct{}, len(edges)) adjacency := make(map[string][]string) for _, dependency := range edges { if err := dependency.Validate(); err != nil { return err } key := dependencyKey(dependency) if _, exists := seen[key]; exists { return fmt.Errorf("duplicate service dependency: %s", key) } seen[key] = struct{}{} if dependency.RelationType == RelationDependsOn { adjacency[dependency.ServiceID] = append(adjacency[dependency.ServiceID], dependency.DependsOnServiceID) } } for source := range adjacency { sort.Strings(adjacency[source]) } colors := make(map[string]uint8) var visit func(string) error visit = func(node string) error { switch colors[node] { case 1: return fmt.Errorf("%w includes %s", ErrDependencyCycle, node) case 2: return nil } colors[node] = 1 for _, target := range adjacency[node] { if err := visit(target); err != nil { return err } } colors[node] = 2 return nil } nodes := make([]string, 0, len(adjacency)) for node := range adjacency { nodes = append(nodes, node) } sort.Strings(nodes) for _, node := range nodes { if err := visit(node); err != nil { return err } } return nil } func SortDependencies(dependencies []Dependency) { sort.SliceStable(dependencies, func(i, j int) bool { left, right := dependencies[i], dependencies[j] if left.ServiceID != right.ServiceID { return left.ServiceID < right.ServiceID } if left.DependsOnServiceID != right.DependsOnServiceID { return left.DependsOnServiceID < right.DependsOnServiceID } if left.RelationType != right.RelationType { return left.RelationType < right.RelationType } if left.SourceID != right.SourceID { return left.SourceID < right.SourceID } return left.ID < right.ID }) } func BuildSuppressionInputs(dependencies []Dependency, states map[string]string) ([]DependencySuppressionInput, error) { if states == nil { states = map[string]string{} } if err := ValidateDependencyGraph(dependencies); err != nil { return nil, err } ordered := append([]Dependency(nil), dependencies...) SortDependencies(ordered) inputs := make([]DependencySuppressionInput, 0, len(ordered)) for _, dependency := range ordered { if dependency.RelationType != RelationDependsOn { continue } state := states[dependency.DependsOnServiceID] input := DependencySuppressionInput{DependencyID: dependency.ID, ServiceID: dependency.ServiceID, DependsOnServiceID: dependency.DependsOnServiceID, SourceID: dependency.SourceID, Confidence: dependency.Confidence, Confirmed: dependency.Confirmed, UpstreamState: state} switch state { case StateDown, StateDegraded: input.Suppress = dependency.Confirmed || dependency.Confidence >= .75 if input.Suppress && dependency.Confirmed { input.Reason = "confirmed_dependency_failure" } else if input.Suppress { input.Reason = "inferred_dependency_failure" } else { input.Reason = "low_confidence_dependency_failure" } case StateUnknown: input.Reason = "dependency_unknown" default: input.Reason = "dependency_healthy" } inputs = append(inputs, input) } return inputs, nil } func dependencyKey(dependency Dependency) string { return dependency.ServiceID + "|" + dependency.DependsOnServiceID + "|" + dependency.RelationType + "|" + dependency.SourceID } var ErrDependencyCycle = errors.New("service dependency cycle")