Files
ConfigKit/internal/engine/inheritance_schedule.go

240 lines
9.7 KiB
Go

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
if diagnostic := chargeInheritanceTemplateHistory(counter, templateResult, node); diagnostic != nil {
return nil, []Diagnostic{*diagnostic}
}
}
if diagnostics := finalizeInheritanceScope(adapter, result, templates, resolved, resolvedOrigins, resolvedPolicies, inputOrigins, limits); len(diagnostics) != 0 {
return nil, diagnostics
}
return result, nil
}