package engine import ( "slices" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) type inheritanceTemplate struct { name string entry document.Entry path Path collectionPolicies collectionPolicyScope parent int hasParent bool hasDirective bool cancelled bool directive document.Entry } type inheritanceGraph struct { scope *document.Node scopePath Path collectionPolicies collectionPolicyScope applicationPolicy sequencePolicy nodes []inheritanceTemplate edges []dependencyEdge byName map[string]int } // analyzeInheritanceGraph retains only direct mapping siblings as graph nodes. // It validates declarations separately from applying their templates, leaving // nested scopes for the existing destination-local walk. func analyzeInheritanceGraph(scope *document.Node, scopePath Path) (*inheritanceGraph, []Diagnostic) { return analyzeInheritanceGraphWithPolicies(scope, scopePath, collectionPolicyScope{}) } func analyzeInheritanceGraphWithPolicies(scope *document.Node, scopePath Path, policies collectionPolicyScope) (*inheritanceGraph, []Diagnostic) { return analyzeInheritanceGraphWithCollectionRules(scope, scopePath, policies, sequencePolicy{}) } func analyzeInheritanceGraphWithCollectionRules(scope *document.Node, scopePath Path, policies collectionPolicyScope, applicationPolicy sequencePolicy) (*inheritanceGraph, []Diagnostic) { if scope == nil || scope.Kind != document.Mapping { return nil, []Diagnostic{{ Code: CodeInheritanceInvalid, Severity: SeverityError, Path: scopePath, Message: "inheritance scope must be a mapping", }} } policyIndex := newInheritancePolicyIndex(policies) graph := &inheritanceGraph{ scope: scope, scopePath: scopePath, collectionPolicies: policyIndex.absoluteScope(scopePath), applicationPolicy: applicationPolicy, byName: make(map[string]int, len(scope.Mapping)), } entries := make(map[string]document.Entry, len(scope.Mapping)) for _, entry := range scope.Mapping { entries[entry.Key] = entry } names := make([]string, 0, len(scope.Mapping)) for name, entry := range entries { if entry.Value.Kind == document.Mapping { names = append(names, name) } } slices.Sort(names) for _, name := range names { index := len(graph.nodes) graph.byName[name] = index graph.nodes = append(graph.nodes, inheritanceTemplate{ name: name, entry: entries[name], path: scopePath.Key(name), collectionPolicies: policyIndex.localize(scopePath.Key(name)), }) } for _, name := range names { index := graph.byName[name] node := &graph.nodes[index] reference, found := inheritanceDirective(node.entry.Value) if !found { continue } node.hasDirective = true node.directive = reference referencePath := node.path.Key(inheritanceKey) location := publicLocation(reference.Value.Location) switch reference.Value.Kind { case document.Null: node.cancelled = true case document.String: parent, exists := entries[reference.Value.Text] if !exists { return nil, []Diagnostic{inheritanceDiagnostic(CodeInheritanceMissing, referencePath, location, nil, "referenced sibling does not exist")} } if parent.Value.Kind != document.Mapping { related := []Location{publicLocation(parent.Value.Location)} return nil, []Diagnostic{inheritanceDiagnostic(CodeInheritanceInvalid, referencePath, location, related, "referenced sibling is not a mapping")} } node.parent = graph.byName[reference.Value.Text] node.hasParent = true graph.edges = append(graph.edges, dependencyEdge{prerequisite: node.parent, dependent: index}) default: return nil, []Diagnostic{inheritanceDiagnostic(CodeInheritanceInvalid, referencePath, location, nil, "inheritance reference must be a sibling name or null")} } } return graph, nil } func (graph *inheritanceGraph) schedule() ([]int, []Diagnostic) { keys := make([]dependencyKey, len(graph.nodes)) for i, node := range graph.nodes { keys[i] = dependencyKey{"definition", graph.scopePath.String(), node.path.String()} } order, cycle := scheduleDependencies(keys, graph.edges) if len(cycle) == 0 { return order, nil } parent := graph.nodes[cycle[0]] child := graph.nodes[cycle[1]] referencePath := child.path.Key(inheritanceKey) location := publicLocation(child.directive.Value.Location) return nil, []Diagnostic{inheritanceDiagnostic(CodeInheritanceCycle, referencePath, location, []Location{publicLocation(parent.entry.Value.Location)}, "sibling inheritance contains a cycle")} } func expandSiblingDefinitionsWithOrigins(scope *document.Node, scopePath Path, limits projectionLimits, inputOrigins map[string]Origin, budget ...*retainedNodeCounter) (*inheritanceScope, []Diagnostic) { return expandSiblingDefinitionsWithPolicies(scope, scopePath, limits, inputOrigins, collectionPolicyScope{}, budget...) } func expandSiblingDefinitionsWithPolicies(scope *document.Node, scopePath Path, limits projectionLimits, inputOrigins map[string]Origin, policies collectionPolicyScope, budget ...*retainedNodeCounter) (*inheritanceScope, []Diagnostic) { return expandSiblingDefinitionsWithCollectionRules(scope, scopePath, limits, inputOrigins, policies, sequencePolicy{}, budget...) } func expandSiblingDefinitionsWithCollectionRules(scope *document.Node, scopePath Path, limits projectionLimits, inputOrigins map[string]Origin, policies collectionPolicyScope, applicationPolicy sequencePolicy, budget ...*retainedNodeCounter) (*inheritanceScope, []Diagnostic) { graph, diagnostics := analyzeInheritanceGraphWithCollectionRules(scope, scopePath, policies, applicationPolicy) if len(diagnostics) != 0 { return nil, diagnostics } order, diagnostics := graph.schedule() if len(diagnostics) != 0 { return nil, diagnostics } result, diagnostics := applyInheritanceTemplates(graph, order, limits, inputOrigins, budget...) if len(diagnostics) != 0 { return nil, diagnostics } return result, nil } // applyInheritanceTemplates builds detached templates in the supplied // dependency-ready order while retaining the existing origins, merge history, // and node-accounting evidence. The root keeps its authored mapping order. func applyInheritanceTemplates(graph *inheritanceGraph, order []int, limits projectionLimits, inputOrigins map[string]Origin, budget ...*retainedNodeCounter) (*inheritanceScope, []Diagnostic) { return applyInheritanceScopeTemplates(mappingInheritanceScope{graph: graph}, order, limits, inputOrigins, budget...) } func applyInheritanceScopeTemplates(adapter inheritanceScopeAdapter, order []int, limits projectionLimits, inputOrigins map[string]Origin, budget ...*retainedNodeCounter) (*inheritanceScope, []Diagnostic) { templates := adapter.templates() resolved := make([]*document.Node, len(templates)) resolvedOrigins := make([]map[string]Origin, len(templates)) resolvedPolicyTemplates := make([][]collectionPolicyScope, len(templates)) resolvedPolicies := make([]collectionPolicyScope, len(templates)) result := &inheritanceScope{} counter := newRetainedNodeCounter(historyNodeLimit) if len(budget) != 0 { counter = budget[0] } retained := 1 // scope container node for _, slot := range adapter.slots() { if slot.hasTemplate || slot.value.Kind == document.Mapping { continue } remaining := limits.nodes - retained - 1 if limits.nodes >= 0 && remaining < 0 { return nil, []Diagnostic{inheritanceBudgetDiagnostic(slot.path, slot.value.Location)} } count, within := inheritanceLowerBoundCount(slot.value, remaining) if !within { return nil, []Diagnostic{inheritanceBudgetDiagnostic(slot.path, slot.value.Location)} } retained += 1 + count } for _, index := range order { node := templates[index] child := prepareInheritanceChild(node, inputOrigins) if limits.nodes >= 0 { remaining := limits.nodes - retained - adapter.slotOverhead() within := inheritanceLowerBoundWithin(child.payload, remaining) if node.hasParent { within = inheritanceOverlayLowerBoundWithin(resolved[node.parent], child.payload, remaining) } if !within { return nil, []Diagnostic{inheritanceBudgetDiagnostic(node.path, node.entry.Value.Location)} } } var parent *preparedInheritanceParent var parentOrigins map[string]Origin if node.hasParent { prepared := prepareInheritanceParent( node, templates[node.parent].path, resolved[node.parent], resolvedPolicyTemplates[node.parent], resolvedPolicies[node.parent], child, adapter.applicationPolicy(), ) parent = &prepared parentOrigins = resolvedOrigins[node.parent] } templateResult, diagnostics := combineInheritanceTemplate( node, child, parent, parentOrigins, adapter.applicationPolicy(), ) if len(diagnostics) != 0 { return nil, diagnostics } resolved[index] = templateResult.value resolvedOrigins[index] = templateResult.origins resolvedPolicies[index] = templateResult.collectionPolicies resolvedPolicyTemplates[index] = templateResult.policyTemplates result.controls = append(result.controls, templateResult.controls...) result.transfers = append(result.transfers, templateResult.transfers...) result.replacements = append(result.replacements, templateResult.replacements...) result.operations = append(result.operations, templateResult.operations...) count, _ := inheritanceLowerBoundCount(resolved[index], limits.nodes) retained += adapter.slotOverhead() + count for _, operation := range templateResult.operations { if !counter.add(operation.parent) || !counter.add(operation.effective) { return nil, []Diagnostic{inheritanceHistoryBudgetDiagnostic(node.path, node.entry.Value.Location)} } } for _, replacement := range templateResult.replacements { if !counter.add(replacement.before) || !counter.add(replacement.after) { return nil, []Diagnostic{inheritanceHistoryBudgetDiagnostic(node.path, node.entry.Value.Location)} } } } root := adapter.rebuild(resolved) result.collectionPolicies = collectionPolicyScope{} result.origins = directInheritanceOrigins(&document.Node{Kind: root.Kind, Location: root.Location}, adapter.scopePath(), inputOrigins) policyIndex := newInheritancePolicyIndex(adapter.collectionPolicies()) for _, slot := range adapter.slots() { if slot.hasTemplate { index := slot.template projectInheritancePolicyRoots(&result.collectionPolicies, resolvedPolicies[index], templates[index].path) for pointer, origin := range resolvedOrigins[index] { result.origins[pointer] = origin } continue } localPolicies := policyIndex.localize(slot.path) projectInheritancePolicyRoots(&result.collectionPolicies, localPolicies, slot.path) for pointer, origin := range directInheritanceOrigins(slot.value, slot.path, inputOrigins) { result.origins[pointer] = origin } } result.root = root if !inheritanceLowerBoundWithin(result.root, limits.nodes) { return nil, []Diagnostic{inheritanceBudgetDiagnostic(adapter.scopePath(), adapter.scope().Location)} } return result, nil }