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", }} } graph := &inheritanceGraph{ scope: scope, scopePath: scopePath, collectionPolicies: cloneCollectionPolicyScopeForInheritance(policies), 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: inheritancePolicyScopeAtPath(policies, 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] templatePolicies := cloneCollectionPolicyScopeForInheritance(node.collectionPolicies) 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.directivePath, 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 - adapter.slotOverhead() 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] parentPolicies := cloneInheritancePolicyTemplates(resolvedPolicyTemplates[node.parent]) effectivePolicies := append(cloneInheritancePolicyTemplates(parentPolicies), cloneCollectionPolicyScopeForInheritance(templatePolicies)) parentPolicyScope := projectInheritanceCollectionPolicies(resolvedPolicies[node.parent], node.path) childPolicyScope := projectCurrentInheritanceCollectionPolicies(templatePolicies, node.path) merged := inheritanceParentPayload(parent, node.identityFields) mergedPolicyScope, policyDiagnostics := mergeInheritanceCollectionPolicies( parentPolicyScope, childPolicyScope, node.path, parent, adapter.applicationPolicy(), ) if len(policyDiagnostics) != 0 { return nil, policyDiagnostics } transfer := inheritanceTransfer{ from: templates[node.parent].path, to: node.path, reference: node.directivePath, locations: []Location{publicLocation(node.directive.Value.Location)}, policyTemplates: parentPolicies, templatePolicies: cloneCollectionPolicyScopeForInheritance(templatePolicies), projectedPolicies: projectInheritancePolicyTemplates( []collectionPolicyScope{resolvedPolicies[node.parent]}, node.path, ), } mergedOrigins := transferredInheritanceOrigins(resolvedOrigins[node.parent], transfer.from, node.path, transfer) mergeInheritanceOrigin(mergedOrigins, node.path, childOrigins[node.path.String()]) var sequenceContributions []sequenceHistoryContribution var combinationDiagnostics []Diagnostic 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) }, merge: func(childPath Path, existing, incoming *document.Node) (*document.Node, bool) { resolution, _, _, active, issues := inheritanceCollectionResolution( childPath, parentPolicyScope, childPolicyScope, adapter.applicationPolicy(), ) if len(issues) != 0 { combinationDiagnostics = append(combinationDiagnostics, issues...) return existing, false } if !active { return nil, false } lowerPath, within := relativeHistoryPath(node.path, childPath) if !within { return nil, false } lowerValue, _ := documentAtPath(parent, lowerPath) combined, problems := combineSequence(lowerValue, incoming, resolution, sequenceCombinationContext{destination: childPath}) if len(problems) != 0 { combinationDiagnostics = append(combinationDiagnostics, problems...) return existing, false } policy := applyInheritanceSequencePolicy( childPath, combined, resolution, parentPolicyScope, childPolicyScope, &mergedPolicyScope, ) mergedPolicyScope.set(childPath, policy) applyInheritanceSequenceOrigins( result, mergedOrigins, childOrigins, childPath, lowerValue, incoming, combined, resolution.operation, ) history := sequenceHistoryContribution{path: childPath.clone(), mode: sequenceHistoryMapped} if resolution.operation == sequenceModeReplace { history.mode = sequenceHistoryReplace if incoming != nil && incoming.Kind == document.Sequence { history.length = len(incoming.Sequence) } } else { history.items = append([]sequenceItemProjection(nil), combined.higherItems...) history.replacements = append([]sequenceItemReplacement(nil), combined.replacements...) } sequenceContributions = append(sequenceContributions, history) return combined.value, true }, }) preserveInheritanceIdentityFields(merged, payload, node.identityFields) if len(combinationDiagnostics) != 0 { return nil, canonicalDiagnostics(combinationDiagnostics) } if problems := validateRegisteredInheritanceSequences(merged, node.path, adapter.applicationPolicy()); len(problems) != 0 { return nil, problems } merged.Location = node.entry.Value.Location resolved[index], resolvedOrigins[index] = merged, mergedOrigins resolvedPolicies[index] = inheritancePolicyScopeAtPath(mergedPolicyScope, node.path) result.transfers = append(result.transfers, transfer) result.operations = append(result.operations, inheritanceOperation{ transfer: &transfer, parent: parent, effective: merged, parentOrigins: resolvedOrigins[node.parent], effectiveOrigins: mergedOrigins, parentPolicies: cloneInheritancePolicyTemplates(parentPolicies), templatePolicies: cloneCollectionPolicyScopeForInheritance(templatePolicies), effectivePolicies: cloneInheritancePolicyTemplates(effectivePolicies), sequences: cloneSequenceHistoryContributions(sequenceContributions), }) resolvedPolicyTemplates[index] = effectivePolicies } else { resolved[index] = document.Clone(payload) resolvedOrigins[index] = childOrigins resolvedPolicyTemplates[index] = []collectionPolicyScope{templatePolicies} resolvedPolicies[index] = cloneCollectionPolicyScopeForInheritance(templatePolicies) } count, _ := inheritanceLowerBoundCount(resolved[index], limits.nodes) retained += adapter.slotOverhead() + 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 := adapter.rebuild(resolved) result.collectionPolicies = collectionPolicyScope{} result.origins = directInheritanceOrigins(&document.Node{Kind: root.Kind, Location: root.Location}, adapter.scopePath(), inputOrigins) 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 := inheritancePolicyScopeAtPath(adapter.collectionPolicies(), 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 }