Files
ConfigKit/internal/engine/inheritance_schedule.go

372 lines
17 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",
}}
}
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)
var replaced collectionReplacementPaths
if len(parentPolicyScope.collections) != 0 || len(childPolicyScope.collections) != 0 {
claims := compositionClaims{byPath: make(map[string][]compositionClaim)}
claims.append(payload, node.path, 0)
declarations := make(map[string][]sequenceDeclarationSpec, len(childPolicyScope.collections))
for key, entry := range childPolicyScope.collections {
declarations[key] = inheritanceDeclarationsAt(entry.destination, entry.policy)
}
parentPolicyScope, replaced = lowerCollectionPoliciesForClaims(parentPolicyScope, claims.ordered, declarations, adapter.applicationPolicy())
}
merged := inheritanceParentPayload(parent, node.identityFields)
mergedPolicyScope, policyDiagnostics := mergeInheritanceCollectionPolicies(
parentPolicyScope, childPolicyScope, node.path, parent, payload, adapter.applicationPolicy(), replaced,
)
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(), incoming,
)
if len(issues) != 0 {
combinationDiagnostics = append(combinationDiagnostics, issues...)
return existing, false
}
if !active {
return nil, false
}
lowerValue := existing
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.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
}