Schedule sibling inheritance by dependency readiness
This commit is contained in:
@@ -220,6 +220,13 @@ value of null cancels a relationship without looking up a parent. Other value
|
|||||||
types, missing or non-mapping parents, and cycles fail the load with value-free
|
types, missing or non-mapping parents, and cycles fail the load with value-free
|
||||||
diagnostics.
|
diagnostics.
|
||||||
|
|
||||||
|
Within each sibling scope, direct mapping definitions resolve by dependency
|
||||||
|
readiness. A parent is resolved before a child; among currently eligible
|
||||||
|
definitions, the lexically smallest canonical semantic pointer is selected,
|
||||||
|
including newly eligible definitions as soon as their parent completes. This
|
||||||
|
ordering governs inheritance operations and their history, controls, and
|
||||||
|
transfers. It does not reorder the mappings in the resulting configuration.
|
||||||
|
|
||||||
`extends` is reserved structural input across ordinary and layered loads, every
|
`extends` is reserved structural input across ordinary and layered loads, every
|
||||||
source kind, imports, selected overlays, and explicit CLI overrides. It is
|
source kind, imports, selected overlays, and explicit CLI overrides. It is
|
||||||
consumed before typed decoding and need not appear in the application's Go
|
consumed before typed decoding and need not appear in the application's Go
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ Implemented component owners:
|
|||||||
and sequence-item placement, definition-registered distinct-ID sequence
|
and sequence-item placement, definition-registered distinct-ID sequence
|
||||||
composition within strict tiers, and immediate-parent-relative import
|
composition within strict tiers, and immediate-parent-relative import
|
||||||
placement within that assembly. Higher-priority sequences replace lower
|
placement within that assembly. Higher-priority sequences replace lower
|
||||||
sequences as whole values. The engine also owns destination-local sibling
|
sequences as whole values. The engine also owns dependency-ready,
|
||||||
inheritance before typed decoding, plus shared source-version checking with
|
destination-local sibling inheritance before typed decoding, plus shared
|
||||||
sanitized declaration evidence.
|
source-version checking with sanitized declaration evidence.
|
||||||
It retains each source's effective placement alongside physical source
|
It retains each source's effective placement alongside physical source
|
||||||
identity and owns schema
|
identity and owns schema
|
||||||
inspection, lifecycle coordination, callback auditing,
|
inspection, lifecycle coordination, callback auditing,
|
||||||
@@ -67,4 +67,5 @@ post-assembly structural operation. [ADR 0012](../adr/0012-relative-import-desti
|
|||||||
records the relative-import extension; [ADR 0013](../adr/0013-shared-source-version-checks.md)
|
records the relative-import extension; [ADR 0013](../adr/0013-shared-source-version-checks.md)
|
||||||
records the source-version decision.
|
records the source-version decision.
|
||||||
[ADR 0014](../adr/0014-sequence-placement-and-composition.md) records explicit
|
[ADR 0014](../adr/0014-sequence-placement-and-composition.md) records explicit
|
||||||
sequence placement and composition.
|
sequence placement and composition. [ADR 0015](../adr/0015-shared-dependency-ready-ordering.md)
|
||||||
|
records the shared dependency-ready ordering decision.
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
package engine
|
package engine
|
||||||
|
|
||||||
import (
|
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||||
"slices"
|
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
||||||
)
|
|
||||||
|
|
||||||
const inheritanceKey = "extends"
|
const inheritanceKey = "extends"
|
||||||
|
|
||||||
@@ -66,13 +62,6 @@ type inheritanceContributor struct {
|
|||||||
trace *inheritanceTrace
|
trace *inheritanceTrace
|
||||||
}
|
}
|
||||||
|
|
||||||
type siblingFrame struct {
|
|
||||||
name string
|
|
||||||
parent string
|
|
||||||
hasParent bool
|
|
||||||
entered bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// expandSiblingDefinitions resolves only the mappings directly in scope.
|
// expandSiblingDefinitions resolves only the mappings directly in scope.
|
||||||
// Nested instructions remain in the returned templates for destination-local
|
// Nested instructions remain in the returned templates for destination-local
|
||||||
// resolution. This private entry point does not participate in public loading.
|
// resolution. This private entry point does not participate in public loading.
|
||||||
@@ -84,202 +73,6 @@ func expandSiblingDefinitionsWithLimits(scope *document.Node, scopePath Path, li
|
|||||||
return expandSiblingDefinitionsWithOrigins(scope, scopePath, limits, nil)
|
return expandSiblingDefinitionsWithOrigins(scope, scopePath, limits, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func expandSiblingDefinitionsWithFrames(scope *document.Node, scopePath Path, limits projectionLimits, inputOrigins map[string]Origin, budget ...*retainedNodeCounter) (*inheritanceScope, []Diagnostic) {
|
|
||||||
if scope == nil || scope.Kind != document.Mapping {
|
|
||||||
return nil, []Diagnostic{{
|
|
||||||
Code: CodeInheritanceInvalid, Severity: SeverityError, Path: scopePath,
|
|
||||||
Message: "inheritance scope must be a mapping",
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
byName := make(map[string]document.Entry, len(scope.Mapping))
|
|
||||||
names := make([]string, 0, len(scope.Mapping))
|
|
||||||
for _, entry := range scope.Mapping {
|
|
||||||
byName[entry.Key] = entry
|
|
||||||
names = append(names, entry.Key)
|
|
||||||
}
|
|
||||||
slices.Sort(names)
|
|
||||||
|
|
||||||
resolved := make(map[string]*document.Node, len(byName))
|
|
||||||
resolvedOrigins := make(map[string]map[string]Origin, len(byName))
|
|
||||||
state := make(map[string]uint8, len(byName))
|
|
||||||
result := &inheritanceScope{}
|
|
||||||
counter := newRetainedNodeCounter(historyNodeLimit)
|
|
||||||
if len(budget) != 0 {
|
|
||||||
counter = budget[0]
|
|
||||||
}
|
|
||||||
retained := 1 // scope mapping node
|
|
||||||
for _, entry := range scope.Mapping {
|
|
||||||
if entry.Value.Kind == document.Mapping {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
remaining := limits.nodes - retained - 1
|
|
||||||
if limits.nodes >= 0 && remaining < 0 {
|
|
||||||
return nil, []Diagnostic{inheritanceBudgetDiagnostic(scopePath.Key(entry.Key), entry.Value.Location)}
|
|
||||||
}
|
|
||||||
count, within := inheritanceLowerBoundCount(entry.Value, remaining)
|
|
||||||
if !within {
|
|
||||||
return nil, []Diagnostic{inheritanceBudgetDiagnostic(scopePath.Key(entry.Key), entry.Value.Location)}
|
|
||||||
}
|
|
||||||
retained += 1 + count // key and value subtree
|
|
||||||
}
|
|
||||||
for _, name := range names {
|
|
||||||
if byName[name].Value.Kind != document.Mapping || state[name] == 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
stack := []siblingFrame{{name: name}}
|
|
||||||
for len(stack) != 0 {
|
|
||||||
frame := &stack[len(stack)-1]
|
|
||||||
entry := byName[frame.name]
|
|
||||||
path := scopePath.Key(frame.name)
|
|
||||||
if !frame.entered {
|
|
||||||
state[frame.name] = 1
|
|
||||||
frame.entered = true
|
|
||||||
reference, found := inheritanceDirective(entry.Value)
|
|
||||||
if found {
|
|
||||||
referencePath := path.Key(inheritanceKey)
|
|
||||||
location := publicLocation(reference.Value.Location)
|
|
||||||
result.controls = append(result.controls, inheritanceControl{
|
|
||||||
path: referencePath, locations: []Location{location},
|
|
||||||
cancelled: reference.Value.Kind == document.Null,
|
|
||||||
})
|
|
||||||
switch reference.Value.Kind {
|
|
||||||
case document.Null:
|
|
||||||
case document.String:
|
|
||||||
frame.parent, frame.hasParent = reference.Value.Text, true
|
|
||||||
parent, exists := byName[frame.parent]
|
|
||||||
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")}
|
|
||||||
}
|
|
||||||
if state[frame.parent] == 1 {
|
|
||||||
related := []Location{publicLocation(parent.Value.Location)}
|
|
||||||
return nil, []Diagnostic{inheritanceDiagnostic(CodeInheritanceCycle, referencePath, location, related,
|
|
||||||
"sibling inheritance contains a cycle")}
|
|
||||||
}
|
|
||||||
if state[frame.parent] == 0 {
|
|
||||||
stack = append(stack, siblingFrame{name: frame.parent})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, []Diagnostic{inheritanceDiagnostic(CodeInheritanceInvalid, referencePath, location, nil,
|
|
||||||
"inheritance reference must be a sibling name or null")}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
payload := withoutInheritanceDirective(entry.Value)
|
|
||||||
operationStart, replacementStart := len(result.operations), len(result.replacements)
|
|
||||||
childOrigins := directInheritanceOrigins(payload, path, inputOrigins)
|
|
||||||
if reference, found := inheritanceDirective(entry.Value); found {
|
|
||||||
control := inheritanceControl{
|
|
||||||
path: path.Key(inheritanceKey),
|
|
||||||
locations: []Location{publicLocation(reference.Value.Location)},
|
|
||||||
cancelled: reference.Value.Kind == document.Null,
|
|
||||||
}
|
|
||||||
controlOrigins := directInheritanceOrigins(reference.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 := false
|
|
||||||
if frame.hasParent {
|
|
||||||
within = inheritanceOverlayLowerBoundWithin(resolved[frame.parent], payload, remaining)
|
|
||||||
} else {
|
|
||||||
within = inheritanceLowerBoundWithin(payload, remaining)
|
|
||||||
}
|
|
||||||
if !within {
|
|
||||||
return nil, []Diagnostic{inheritanceBudgetDiagnostic(path, entry.Value.Location)}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if frame.hasParent {
|
|
||||||
parent := resolved[frame.parent]
|
|
||||||
merged := document.Clone(parent)
|
|
||||||
control := inheritanceDirectiveLocation(entry.Value)
|
|
||||||
transfer := inheritanceTransfer{
|
|
||||||
from: scopePath.Key(frame.parent), to: path,
|
|
||||||
reference: path.Key(inheritanceKey), locations: []Location{control},
|
|
||||||
}
|
|
||||||
mergedOrigins := transferredInheritanceOrigins(resolvedOrigins[frame.parent], transfer.from, path, transfer)
|
|
||||||
mergeInheritanceOrigin(mergedOrigins, path, childOrigins[path.String()])
|
|
||||||
mergeRecursiveMappings(merged, payload, 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 = entry.Value.Location
|
|
||||||
resolved[frame.name] = merged
|
|
||||||
resolvedOrigins[frame.name] = mergedOrigins
|
|
||||||
result.transfers = append(result.transfers, transfer)
|
|
||||||
result.operations = append(result.operations, inheritanceOperation{
|
|
||||||
transfer: &transfer, parent: parent, effective: merged,
|
|
||||||
parentOrigins: resolvedOrigins[frame.parent], effectiveOrigins: mergedOrigins,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
resolved[frame.name] = document.Clone(payload)
|
|
||||||
resolvedOrigins[frame.name] = childOrigins
|
|
||||||
}
|
|
||||||
count, _ := inheritanceLowerBoundCount(resolved[frame.name], limits.nodes)
|
|
||||||
retained += 1 + count
|
|
||||||
for _, operation := range result.operations[operationStart:] {
|
|
||||||
if !counter.add(operation.parent) || !counter.add(operation.effective) {
|
|
||||||
return nil, []Diagnostic{inheritanceHistoryBudgetDiagnostic(path, entry.Value.Location)}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, replacement := range result.replacements[replacementStart:] {
|
|
||||||
if !counter.add(replacement.before) || !counter.add(replacement.after) {
|
|
||||||
return nil, []Diagnostic{inheritanceHistoryBudgetDiagnostic(path, entry.Value.Location)}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
state[frame.name] = 2
|
|
||||||
stack = stack[:len(stack)-1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
root := *scope
|
|
||||||
root.Mapping = make([]document.Entry, len(scope.Mapping))
|
|
||||||
rootOrigin := directInheritanceOrigins(&document.Node{Kind: document.Mapping, Location: scope.Location}, scopePath, inputOrigins)
|
|
||||||
result.origins = rootOrigin
|
|
||||||
for index, entry := range scope.Mapping {
|
|
||||||
root.Mapping[index] = entry
|
|
||||||
if resolved[entry.Key] != nil {
|
|
||||||
root.Mapping[index].Value = resolved[entry.Key]
|
|
||||||
for pointer, origin := range resolvedOrigins[entry.Key] {
|
|
||||||
result.origins[pointer] = origin
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
root.Mapping[index].Value = document.Clone(entry.Value)
|
|
||||||
for pointer, origin := range directInheritanceOrigins(entry.Value, scopePath.Key(entry.Key), inputOrigins) {
|
|
||||||
result.origins[pointer] = origin
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.root = &root
|
|
||||||
if !inheritanceLowerBoundWithin(result.root, limits.nodes) {
|
|
||||||
return nil, []Diagnostic{inheritanceBudgetDiagnostic(scopePath, scope.Location)}
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func inheritanceDirective(node *document.Node) (document.Entry, bool) {
|
func inheritanceDirective(node *document.Node) (document.Entry, bool) {
|
||||||
for _, entry := range node.Mapping {
|
for _, entry := range node.Mapping {
|
||||||
if entry.Key == inheritanceKey {
|
if entry.Key == inheritanceKey {
|
||||||
|
|||||||
@@ -25,84 +25,6 @@ type inheritanceGraph struct {
|
|||||||
byName map[string]int
|
byName map[string]int
|
||||||
}
|
}
|
||||||
|
|
||||||
// depthFirstTemplateOrder preserves the currently wired parent-first order
|
|
||||||
// while the loading path adopts the graph/application boundary.
|
|
||||||
func (graph *inheritanceGraph) depthFirstTemplateOrder() ([]int, []Diagnostic) {
|
|
||||||
const (
|
|
||||||
unvisited = uint8(0)
|
|
||||||
active = uint8(1)
|
|
||||||
finished = uint8(2)
|
|
||||||
)
|
|
||||||
state := make([]uint8, len(graph.nodes))
|
|
||||||
order := make([]int, 0, len(graph.nodes))
|
|
||||||
for start := range graph.nodes {
|
|
||||||
if state[start] != unvisited {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
stack := []int{start}
|
|
||||||
for len(stack) != 0 {
|
|
||||||
index := stack[len(stack)-1]
|
|
||||||
if state[index] == unvisited {
|
|
||||||
state[index] = active
|
|
||||||
node := graph.nodes[index]
|
|
||||||
if node.hasParent {
|
|
||||||
parent := node.parent
|
|
||||||
switch state[parent] {
|
|
||||||
case active:
|
|
||||||
referencePath := node.path.Key(inheritanceKey)
|
|
||||||
location := publicLocation(node.directive.Value.Location)
|
|
||||||
return nil, []Diagnostic{inheritanceDiagnostic(CodeInheritanceCycle, referencePath, location,
|
|
||||||
[]Location{publicLocation(graph.nodes[parent].entry.Value.Location)}, "sibling inheritance contains a cycle")}
|
|
||||||
case unvisited:
|
|
||||||
stack = append(stack, parent)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
state[index] = finished
|
|
||||||
order = append(order, index)
|
|
||||||
stack = stack[:len(stack)-1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return order, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (graph *inheritanceGraph) depthFirstControls() []inheritanceControl {
|
|
||||||
visited := make([]bool, len(graph.nodes))
|
|
||||||
controls := make([]inheritanceControl, 0, len(graph.nodes))
|
|
||||||
type frame struct {
|
|
||||||
index int
|
|
||||||
entered bool
|
|
||||||
}
|
|
||||||
for start := range graph.nodes {
|
|
||||||
if visited[start] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
stack := []frame{{index: start}}
|
|
||||||
for len(stack) != 0 {
|
|
||||||
current := &stack[len(stack)-1]
|
|
||||||
node := graph.nodes[current.index]
|
|
||||||
if !current.entered {
|
|
||||||
current.entered = true
|
|
||||||
visited[current.index] = true
|
|
||||||
if node.hasDirective {
|
|
||||||
controls = append(controls, inheritanceControl{
|
|
||||||
path: node.path.Key(inheritanceKey),
|
|
||||||
locations: []Location{publicLocation(node.directive.Value.Location)},
|
|
||||||
cancelled: node.cancelled,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if node.hasParent && !visited[node.parent] {
|
|
||||||
stack = append(stack, frame{index: node.parent})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stack = stack[:len(stack)-1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return controls
|
|
||||||
}
|
|
||||||
|
|
||||||
// analyzeInheritanceGraph retains only direct mapping siblings as graph nodes.
|
// analyzeInheritanceGraph retains only direct mapping siblings as graph nodes.
|
||||||
// It validates declarations separately from applying their templates, leaving
|
// It validates declarations separately from applying their templates, leaving
|
||||||
// nested scopes for the existing destination-local walk.
|
// nested scopes for the existing destination-local walk.
|
||||||
@@ -197,15 +119,11 @@ func expandSiblingDefinitionsWithOrigins(scope *document.Node, scopePath Path, l
|
|||||||
if len(diagnostics) != 0 {
|
if len(diagnostics) != 0 {
|
||||||
return nil, diagnostics
|
return nil, diagnostics
|
||||||
}
|
}
|
||||||
order, diagnostics := graph.depthFirstTemplateOrder()
|
order, diagnostics := graph.schedule()
|
||||||
if len(diagnostics) != 0 {
|
if len(diagnostics) != 0 {
|
||||||
return nil, diagnostics
|
return nil, diagnostics
|
||||||
}
|
}
|
||||||
result, diagnostics := applyInheritanceTemplates(graph, order, limits, inputOrigins, budget...)
|
return applyInheritanceTemplates(graph, order, limits, inputOrigins, budget...)
|
||||||
if len(diagnostics) == 0 {
|
|
||||||
result.controls = graph.depthFirstControls()
|
|
||||||
}
|
|
||||||
return result, diagnostics
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyInheritanceTemplates builds detached templates in the supplied
|
// applyInheritanceTemplates builds detached templates in the supplied
|
||||||
|
|||||||
@@ -110,6 +110,41 @@ beta:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInheritanceExpansionInterleavesUnequalBranches(t *testing.T) {
|
||||||
|
root := parseCompositionDocument(t, "branches", `a-grandchild:
|
||||||
|
extends: z-child
|
||||||
|
own: grandchild
|
||||||
|
b-base:
|
||||||
|
base: b
|
||||||
|
c-base:
|
||||||
|
base: c
|
||||||
|
d-child:
|
||||||
|
extends: c-base
|
||||||
|
own: d
|
||||||
|
z-child:
|
||||||
|
extends: b-base
|
||||||
|
own: z
|
||||||
|
`)
|
||||||
|
result, diagnostics := expandInheritanceTree(root, defaultProjectionLimits())
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
t.Fatalf("expand diagnostics = %#v", diagnostics)
|
||||||
|
}
|
||||||
|
got := make([]string, len(result.transfers))
|
||||||
|
for i, transfer := range result.transfers {
|
||||||
|
got[i] = transfer.from.String() + " -> " + transfer.to.String()
|
||||||
|
}
|
||||||
|
want := []string{"/c-base -> /d-child", "/b-base -> /z-child", "/z-child -> /a-grandchild"}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("transfer order = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
if value, found := documentAtPath(result.root, Path{}.Key("a-grandchild").Key("base")); !found || value.Text != "b" {
|
||||||
|
t.Fatalf("grandchild inherited value = %#v, found %t", value, found)
|
||||||
|
}
|
||||||
|
if value, found := documentAtPath(result.root, Path{}.Key("d-child").Key("base")); !found || value.Text != "c" {
|
||||||
|
t.Fatalf("independent branch inherited value = %#v, found %t", value, found)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestInheritanceGraphValidatesRelationshipKinds(t *testing.T) {
|
func TestInheritanceGraphValidatesRelationshipKinds(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
Reference in New Issue
Block a user