package engine import ( "slices" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) type inheritanceTemplate struct { name string entry document.Entry path Path parent int hasParent bool hasDirective bool cancelled bool directive document.Entry } type inheritanceGraph struct { scope *document.Node scopePath Path 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) { if scope == nil || scope.Kind != document.Mapping { return nil, []Diagnostic{{ Code: CodeInheritanceInvalid, Severity: SeverityError, Path: scopePath, Message: "inheritance scope must be a mapping", }} } graph := &inheritanceGraph{ scope: scope, scopePath: scopePath, 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), }) } 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) { graph, diagnostics := analyzeInheritanceGraph(scope, scopePath) if len(diagnostics) != 0 { return nil, diagnostics } order, diagnostics := graph.schedule() if len(diagnostics) != 0 { return nil, diagnostics } return applyInheritanceTemplates(graph, order, limits, inputOrigins, budget...) } // 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) { resolved := make([]*document.Node, len(graph.nodes)) resolvedOrigins := make([]map[string]Origin, len(graph.nodes)) result := &inheritanceScope{} counter := newRetainedNodeCounter(historyNodeLimit) if len(budget) != 0 { counter = budget[0] } retained := 1 // scope mapping node for _, entry := range graph.scope.Mapping { if entry.Value.Kind == document.Mapping { continue } remaining := limits.nodes - retained - 1 if limits.nodes >= 0 && remaining < 0 { return nil, []Diagnostic{inheritanceBudgetDiagnostic(graph.scopePath.Key(entry.Key), entry.Value.Location)} } count, within := inheritanceLowerBoundCount(entry.Value, remaining) if !within { return nil, []Diagnostic{inheritanceBudgetDiagnostic(graph.scopePath.Key(entry.Key), entry.Value.Location)} } retained += 1 + count } for _, index := range order { node := graph.nodes[index] payload := withoutInheritanceDirective(node.entry.Value) operationStart, replacementStart := len(result.operations), len(result.replacements) childOrigins := directInheritanceOrigins(payload, node.path, inputOrigins) if node.hasDirective { control := inheritanceControl{ path: node.path.Key(inheritanceKey), locations: []Location{publicLocation(node.directive.Value.Location)}, cancelled: node.cancelled, } result.controls = append(result.controls, control) controlOrigins := directInheritanceOrigins(node.directive.Value, control.path, inputOrigins) result.operations = append(result.operations, inheritanceOperation{ control: &control, controlOrigin: controlOrigins[control.path.String()], }) } if limits.nodes >= 0 { remaining := limits.nodes - retained - 1 // the sibling key within := inheritanceLowerBoundWithin(payload, remaining) if node.hasParent { within = inheritanceOverlayLowerBoundWithin(resolved[node.parent], payload, remaining) } if !within { return nil, []Diagnostic{inheritanceBudgetDiagnostic(node.path, node.entry.Value.Location)} } } if node.hasParent { parent := resolved[node.parent] merged := document.Clone(parent) transfer := inheritanceTransfer{ from: graph.nodes[node.parent].path, to: node.path, reference: node.path.Key(inheritanceKey), locations: []Location{publicLocation(node.directive.Value.Location)}, } mergedOrigins := transferredInheritanceOrigins(resolvedOrigins[node.parent], transfer.from, node.path, transfer) mergeInheritanceOrigin(mergedOrigins, node.path, childOrigins[node.path.String()]) mergeRecursiveMappings(merged, payload, node.path, new(mappingIndex), recursiveMergeHooks{ added: func(childPath Path, incoming *document.Node) { copyInheritanceSubtree(mergedOrigins, childOrigins, incoming, childPath) }, combined: func(childPath Path, _ *document.Node) { mergeInheritanceOrigin(mergedOrigins, childPath, childOrigins[childPath.String()]) }, replaced: func(childPath Path, existing, incoming *document.Node) { previous := displacedOrigins(existing, mergedOrigins, childPath) result.replacements = append(result.replacements, inheritanceReplacement{ path: childPath.clone(), before: existing, after: incoming, previous: previous, effective: cloneOrigin(childOrigins[childPath.String()]), }) deleteOriginSubtree(mergedOrigins, childPath, existing) copyInheritanceSubtree(mergedOrigins, childOrigins, incoming, childPath) }, }) merged.Location = node.entry.Value.Location resolved[index], resolvedOrigins[index] = merged, mergedOrigins result.transfers = append(result.transfers, transfer) result.operations = append(result.operations, inheritanceOperation{ transfer: &transfer, parent: parent, effective: merged, parentOrigins: resolvedOrigins[node.parent], effectiveOrigins: mergedOrigins, }) } else { resolved[index] = document.Clone(payload) resolvedOrigins[index] = childOrigins } count, _ := inheritanceLowerBoundCount(resolved[index], limits.nodes) retained += 1 + count for _, operation := range result.operations[operationStart:] { if !counter.add(operation.parent) || !counter.add(operation.effective) { return nil, []Diagnostic{inheritanceHistoryBudgetDiagnostic(node.path, node.entry.Value.Location)} } } for _, replacement := range result.replacements[replacementStart:] { if !counter.add(replacement.before) || !counter.add(replacement.after) { return nil, []Diagnostic{inheritanceHistoryBudgetDiagnostic(node.path, node.entry.Value.Location)} } } } root := *graph.scope root.Mapping = make([]document.Entry, len(graph.scope.Mapping)) result.origins = directInheritanceOrigins(&document.Node{Kind: document.Mapping, Location: graph.scope.Location}, graph.scopePath, inputOrigins) for i, entry := range graph.scope.Mapping { root.Mapping[i] = entry if index, found := graph.byName[entry.Key]; found { root.Mapping[i].Value = resolved[index] for pointer, origin := range resolvedOrigins[index] { result.origins[pointer] = origin } continue } root.Mapping[i].Value = document.Clone(entry.Value) for pointer, origin := range directInheritanceOrigins(entry.Value, graph.scopePath.Key(entry.Key), inputOrigins) { result.origins[pointer] = origin } } result.root = &root if !inheritanceLowerBoundWithin(result.root, limits.nodes) { return nil, []Diagnostic{inheritanceBudgetDiagnostic(graph.scopePath, graph.scope.Location)} } return result, nil }