package engine import ( "cmp" "slices" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) type inheritanceWalker struct { nodes int // logical non-directive nodes in the entire working tree origins map[string]Origin collectionPolicies collectionPolicyScope applicationPolicy sequencePolicy limits projectionLimits retained *retainedNodeCounter transfers []inheritanceTransfer controls []inheritanceControl replacements []inheritanceReplacement operations []inheritanceOperation } // expandInheritanceTree resolves each destination scope after its enclosing // relationships. The loading lifecycle uses the origin-aware entry point. func expandInheritanceTree(input *document.Node, limits projectionLimits) (*inheritanceScope, []Diagnostic) { return expandInheritanceWithOrigins(input, nil, limits) } func expandInheritanceWithOrigins(input *document.Node, inputOrigins map[string]Origin, limits projectionLimits) (*inheritanceScope, []Diagnostic) { return expandInheritanceWithPolicies(input, inputOrigins, collectionPolicyScope{}, limits) } func expandInheritanceWithBudgets(input *document.Node, inputOrigins map[string]Origin, limits projectionLimits, historyLimit int) (*inheritanceScope, []Diagnostic) { return expandInheritanceWithPolicyBudgets(input, inputOrigins, collectionPolicyScope{}, limits, historyLimit) } func expandInheritanceWithPolicies(input *document.Node, inputOrigins map[string]Origin, policies collectionPolicyScope, limits projectionLimits) (*inheritanceScope, []Diagnostic) { return expandInheritanceWithCollectionRules(input, inputOrigins, policies, sequencePolicy{}, limits) } func expandInheritanceWithPolicyBudgets(input *document.Node, inputOrigins map[string]Origin, policies collectionPolicyScope, limits projectionLimits, historyLimit int) (*inheritanceScope, []Diagnostic) { return expandInheritanceWithCollectionRuleBudgets(input, inputOrigins, policies, sequencePolicy{}, limits, historyLimit) } func expandInheritanceWithCollectionRules(input *document.Node, inputOrigins map[string]Origin, policies collectionPolicyScope, applicationPolicy sequencePolicy, limits projectionLimits) (*inheritanceScope, []Diagnostic) { return expandInheritanceWithCollectionRuleBudgets(input, inputOrigins, policies, applicationPolicy, limits, historyNodeLimit) } func expandInheritanceWithCollectionRuleBudgets(input *document.Node, inputOrigins map[string]Origin, policies collectionPolicyScope, applicationPolicy sequencePolicy, limits projectionLimits, historyLimit int) (*inheritanceScope, []Diagnostic) { if input == nil || input.Kind != document.Mapping { return nil, []Diagnostic{{Code: CodeInheritanceInvalid, Severity: SeverityError, Message: "inheritance requires a mapping document root"}} } root := document.Clone(input) origins := cloneOriginIndex(inputOrigins) if inputOrigins == nil { origins = directInheritanceOrigins(input, Path{}, nil) } nodes, within := inheritanceLowerBoundCount(root, limits.nodes) if !within { return nil, []Diagnostic{inheritanceBudgetDiagnostic(Path{}, root.Location)} } walker := inheritanceWalker{ nodes: nodes, limits: limits, origins: origins, collectionPolicies: cloneCollectionPolicyScopeForInheritance(policies), applicationPolicy: applicationPolicy, retained: newRetainedNodeCounter(historyLimit), } if problem := walker.visit(&root, Path{}, true); problem != nil { return nil, []Diagnostic{*problem} } if problem := inheritanceFinalBudget(root, limits); problem != nil { return nil, []Diagnostic{*problem} } return &inheritanceScope{ root: root, origins: walker.origins, collectionPolicies: cloneCollectionPolicyScopeForInheritance(walker.collectionPolicies), transfers: walker.transfers, controls: walker.controls, replacements: walker.replacements, operations: walker.operations, }, nil } func (w *inheritanceWalker) visit(node **document.Node, path Path, unscoped bool) *Diagnostic { switch (*node).Kind { case document.Mapping: original := *node if unscoped { reference, found := inheritanceDirective(*node) if found { referencePath := path.Key(inheritanceKey) location := publicLocation(reference.Value.Location) if reference.Value.Kind != document.Null { problem := inheritanceDiagnostic(CodeInheritanceInvalid, referencePath, location, nil, "inheritance reference has no enclosing sibling scope") return &problem } w.controls = append(w.controls, inheritanceControl{ path: referencePath, locations: []Location{location}, cancelled: true, }) origin := directInheritanceOrigins(reference.Value, referencePath, w.origins)[referencePath.String()] control := inheritanceControl{path: referencePath, locations: []Location{location}, cancelled: true} w.operations = append(w.operations, inheritanceOperation{control: &control, controlOrigin: origin}) deleteOriginSubtree(w.origins, referencePath, reference.Value) *node = withoutInheritanceDirective(*node) } } if scopeHasInheritanceDefinitions(*node) { // Only this scope can change the logical total. Reserve the nodes // outside it so expansion checks the remaining budget before cloning. before, _ := inheritanceLowerBoundCount(*node, -1) limits := w.limits if limits.nodes >= 0 { limits.nodes -= w.nodes - before } resolved, diagnostics := expandSiblingDefinitionsWithCollectionRules( *node, path, limits, w.origins, w.collectionPolicies, w.applicationPolicy, w.retained, ) if len(diagnostics) != 0 { return &diagnostics[0] } *node = resolved.root for pointer, entry := range w.collectionPolicies.collections { if pathHasPrefix(entry.destination, path) { delete(w.collectionPolicies.collections, pointer) } } for _, entry := range resolved.collectionPolicies.collections { w.collectionPolicies.set(entry.destination, entry.policy) } deleteOriginSubtree(w.origins, path, original) for pointer, origin := range resolved.origins { w.origins[pointer] = origin } w.transfers = append(w.transfers, resolved.transfers...) w.controls = append(w.controls, resolved.controls...) w.replacements = append(w.replacements, resolved.replacements...) w.operations = append(w.operations, resolved.operations...) after, _ := inheritanceLowerBoundCount(*node, -1) w.nodes += after - before } // Operations retain immutable enclosing templates. Detach just this // container before replacing descendants during destination resolution; // untouched subtrees remain shared with the retained evidence. working := **node working.Mapping = slices.Clone(working.Mapping) *node = &working indices := make([]int, len((*node).Mapping)) for index := range indices { indices[index] = index } slices.SortFunc(indices, func(left, right int) int { return cmp.Compare((*node).Mapping[left].Key, (*node).Mapping[right].Key) }) for _, index := range indices { entry := &(*node).Mapping[index] if problem := w.visit(&entry.Value, path.Key(entry.Key), false); problem != nil { return problem } } case document.Sequence: working := **node working.Sequence = slices.Clone(working.Sequence) *node = &working for index := range (*node).Sequence { child := &(*node).Sequence[index] if problem := w.visit(child, path.Index(index), (*child).Kind == document.Mapping); problem != nil { return problem } } } return nil } func scopeHasInheritanceDefinitions(scope *document.Node) bool { for _, entry := range scope.Mapping { if entry.Value.Kind != document.Mapping { continue } if _, found := inheritanceDirective(entry.Value); found { return true } } return false } // A directive may disappear during later destination resolution, so only // non-directive nodes form a safe early lower bound on final output size. func inheritanceLowerBoundWithin(root *document.Node, limit int) bool { _, within := inheritanceLowerBoundCount(root, limit) return within } func inheritanceLowerBoundCount(root *document.Node, limit int) (int, bool) { if limit < 0 { limit = int(^uint(0) >> 1) } stack := []*document.Node{root} count := 0 for len(stack) != 0 { node := stack[len(stack)-1] stack = stack[:len(stack)-1] if node == nil { continue } count++ if count > limit { return count, false } switch node.Kind { case document.Mapping: for _, entry := range node.Mapping { if entry.Key == inheritanceKey { continue } count++ // mapping key stack = append(stack, entry.Value) } case document.Sequence: stack = append(stack, node.Sequence...) } if count > limit { return count, false } } return count, true } // Count the materialized mapping shape before cloning a memoized parent. // Relationship fields are excluded because later destination resolution // consumes them; their omission makes this a safe lower bound. func inheritanceOverlayLowerBoundWithin(lower, upper *document.Node, limit int) bool { if limit < 0 { return false } index := new(mappingIndex) count := 0 var visit func(*document.Node, *document.Node) bool visit = func(base, incoming *document.Node) bool { if base.Kind != document.Mapping || incoming.Kind != document.Mapping { added, within := inheritanceLowerBoundCount(incoming, limit-count) count += added return within } count++ // mapping node if count > limit { return false } for _, entry := range base.Mapping { if entry.Key == inheritanceKey { continue } count++ // mapping key if count > limit { return false } position := index.find(incoming, entry.Key) if position >= 0 { if !visit(entry.Value, incoming.Mapping[position].Value) { return false } } else { added, within := inheritanceLowerBoundCount(entry.Value, limit-count) count += added if !within { return false } } } for _, entry := range incoming.Mapping { if entry.Key == inheritanceKey || index.find(base, entry.Key) >= 0 { continue } count++ // mapping key if count > limit { return false } added, within := inheritanceLowerBoundCount(entry.Value, limit-count) count += added if !within || count > limit { return false } } return true } return visit(lower, upper) } func inheritanceFinalBudget(root *document.Node, limits projectionLimits) *Diagnostic { type pending struct { node *document.Node path Path } stack := []pending{{node: root}} count := 0 for len(stack) != 0 { item := stack[len(stack)-1] stack = stack[:len(stack)-1] if len(item.path.segments) > limits.depth { problem := inheritanceBudgetDiagnostic(item.path, item.node.Location) return &problem } count++ if limits.nodes >= 0 && count > limits.nodes { problem := inheritanceBudgetDiagnostic(item.path, item.node.Location) return &problem } switch item.node.Kind { case document.Mapping: count += len(item.node.Mapping) // mapping keys for index := len(item.node.Mapping) - 1; index >= 0; index-- { entry := item.node.Mapping[index] stack = append(stack, pending{node: entry.Value, path: item.path.Key(entry.Key)}) } case document.Sequence: for index := len(item.node.Sequence) - 1; index >= 0; index-- { stack = append(stack, pending{node: item.node.Sequence[index], path: item.path.Index(index)}) } } if limits.nodes >= 0 && count > limits.nodes { problem := inheritanceBudgetDiagnostic(item.path, item.node.Location) return &problem } } return nil } func inheritanceBudgetDiagnostic(path Path, location document.Location) Diagnostic { primary := publicLocation(location) return Diagnostic{ Code: CodeSchemaProjectionLimit, Severity: SeverityError, Path: path, Message: "expanded configuration exceeds the projection limit", Primary: &primary, } } func inheritanceHistoryBudgetDiagnostic(path Path, location document.Location) Diagnostic { primary := publicLocation(location) return Diagnostic{ Code: CodeExplainLimitExceeded, Severity: SeverityError, Path: path, Message: "retained inheritance history exceeds the node limit", Primary: &primary, } }