Preserve sibling inheritance in configuration history and explanations
This commit is contained in:
@@ -211,6 +211,14 @@ Run the everyday checker.
|
|||||||
|
|
||||||
## Stage 5 — Integrate historical inspection
|
## Stage 5 — Integrate historical inspection
|
||||||
|
|
||||||
|
**Completed 2026-09-29.** Source replay stays at original paths, followed by
|
||||||
|
projected structural control/transfer events before typed reconciliation and
|
||||||
|
application resolution. History and explanations preserve consumed/cancelled
|
||||||
|
directives, equal-valued child replacements, displaced descendants, nested
|
||||||
|
destination results, and parent source updates. Shared retained evidence is
|
||||||
|
counted with a bounded incremental check; focused tests and `./scripts/check.sh`
|
||||||
|
passed. Public loading remains unchanged.
|
||||||
|
|
||||||
Extend `history.go`, `load_metadata.go`, `explain.go`, and explanation rendering.
|
Extend `history.go`, `load_metadata.go`, `explain.go`, and explanation rendering.
|
||||||
Retain original source applications; represent structural expansion separately,
|
Retain original source applications; represent structural expansion separately,
|
||||||
between source replay and canonical decoding, followed by recorded application
|
between source replay and canonical decoding, followed by recorded application
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ func (r *Inspection) Explain(path Path) (Explanation, bool) {
|
|||||||
Origin: historyApplicationOrigin(application, operationPath),
|
Origin: historyApplicationOrigin(application, operationPath),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
appendInheritanceExplanation(&explanation, &working, path, r.inheritance, r.secrets)
|
||||||
|
|
||||||
// Intermediate input replacements do not permanently suppress defaults:
|
// Intermediate input replacements do not permanently suppress defaults:
|
||||||
// decoding applies them to the final input tree. Show that reconciliation
|
// decoding applies them to the final input tree. Show that reconciliation
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ func (r *Inspection) WriteExplanation(output io.Writer, path Path) error {
|
|||||||
rendered.WriteString(" - representation: ")
|
rendered.WriteString(" - representation: ")
|
||||||
if change.Origin.Operation == ChangeDecoding {
|
if change.Origin.Operation == ChangeDecoding {
|
||||||
rendered.WriteString("input to canonical typed\n")
|
rendered.WriteString("input to canonical typed\n")
|
||||||
|
} else if change.Origin.Operation == ChangeInheritance {
|
||||||
|
rendered.WriteString("structural inheritance\n")
|
||||||
} else if change.Origin.Layer == LayerBase || change.Origin.Layer == LayerUnderlay || change.Origin.Layer == LayerOverlay || change.Origin.Layer == LayerCLI {
|
} else if change.Origin.Layer == LayerBase || change.Origin.Layer == LayerUnderlay || change.Origin.Layer == LayerOverlay || change.Origin.Layer == LayerCLI {
|
||||||
rendered.WriteString("policy-validated input\n")
|
rendered.WriteString("policy-validated input\n")
|
||||||
} else {
|
} else {
|
||||||
@@ -91,6 +93,15 @@ func writeExplanationOrigin(output *strings.Builder, label string, origin Origin
|
|||||||
output.WriteString(strconv.Itoa(location.Column))
|
output.WriteString(strconv.Itoa(location.Column))
|
||||||
output.WriteByte('\n')
|
output.WriteByte('\n')
|
||||||
}
|
}
|
||||||
|
for _, step := range origin.Inheritance {
|
||||||
|
output.WriteString(" inherited: from=")
|
||||||
|
output.WriteString(strconv.Quote(step.From.String()))
|
||||||
|
output.WriteString(" to=")
|
||||||
|
output.WriteString(strconv.Quote(step.To.String()))
|
||||||
|
output.WriteString(" reference=")
|
||||||
|
output.WriteString(strconv.Quote(step.Reference.String()))
|
||||||
|
output.WriteByte('\n')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeExplanationSnapshot(output *strings.Builder, label string, snapshot ValueSnapshot, indent int) {
|
func writeExplanationSnapshot(output *strings.Builder, label string, snapshot ValueSnapshot, indent int) {
|
||||||
|
|||||||
@@ -6,6 +6,40 @@ import (
|
|||||||
|
|
||||||
const historyNodeLimit = 4_000_000
|
const historyNodeLimit = 4_000_000
|
||||||
|
|
||||||
|
type retainedNodeCounter struct {
|
||||||
|
seen map[*document.Node]bool
|
||||||
|
count int
|
||||||
|
limit int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRetainedNodeCounter(limit int) *retainedNodeCounter {
|
||||||
|
return &retainedNodeCounter{seen: make(map[*document.Node]bool), limit: limit}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *retainedNodeCounter) add(root *document.Node) bool {
|
||||||
|
if c == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
stack := []*document.Node{root}
|
||||||
|
for len(stack) != 0 {
|
||||||
|
node := stack[len(stack)-1]
|
||||||
|
stack = stack[:len(stack)-1]
|
||||||
|
if node == nil || c.seen[node] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c.seen[node] = true
|
||||||
|
c.count++
|
||||||
|
if c.limit >= 0 && c.count > c.limit {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, entry := range node.Mapping {
|
||||||
|
stack = append(stack, entry.Value)
|
||||||
|
}
|
||||||
|
stack = append(stack, node.Sequence...)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// History applications borrow immutable policy-validated input trees. Metadata
|
// History applications borrow immutable policy-validated input trees. Metadata
|
||||||
// records answer presence and origin queries; replay reconstructs value changes.
|
// records answer presence and origin queries; replay reconstructs value changes.
|
||||||
// Replay reconciles with canonical decoding before domain resolution so both
|
// Replay reconciles with canonical decoding before domain resolution so both
|
||||||
@@ -230,11 +264,11 @@ func replaceHistoryPath(root **document.Node, path Path, replacement *document.N
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func historyNodesWithinLimit(baseline, input, final *document.Node, applications []historyApplication, resolutions []resolutionSnapshot) bool {
|
func historyNodesWithinLimit(baseline, input, final *document.Node, applications []historyApplication, resolutions []resolutionSnapshot, inherited ...*inheritanceScope) bool {
|
||||||
return historyNodesWithin(historyNodeLimit, baseline, input, final, applications, resolutions)
|
return historyNodesWithin(historyNodeLimit, baseline, input, final, applications, resolutions, inherited...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func historyNodesWithin(limit int, baseline, input, final *document.Node, applications []historyApplication, resolutions []resolutionSnapshot) bool {
|
func historyNodesWithin(limit int, baseline, input, final *document.Node, applications []historyApplication, resolutions []resolutionSnapshot, inherited ...*inheritanceScope) bool {
|
||||||
seen := make(map[*document.Node]bool)
|
seen := make(map[*document.Node]bool)
|
||||||
count := 0
|
count := 0
|
||||||
var visit func(*document.Node) bool
|
var visit func(*document.Node) bool
|
||||||
@@ -272,5 +306,23 @@ func historyNodesWithin(limit int, baseline, input, final *document.Node, applic
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, expansion := range inherited {
|
||||||
|
if expansion == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !visit(expansion.root) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, operation := range expansion.operations {
|
||||||
|
if !visit(operation.parent) || !visit(operation.effective) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, replacement := range expansion.replacements {
|
||||||
|
if !visit(replacement.before) || !visit(replacement.after) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,17 @@ type inheritanceScope struct {
|
|||||||
transfers []inheritanceTransfer
|
transfers []inheritanceTransfer
|
||||||
controls []inheritanceControl
|
controls []inheritanceControl
|
||||||
replacements []inheritanceReplacement
|
replacements []inheritanceReplacement
|
||||||
|
operations []inheritanceOperation
|
||||||
|
}
|
||||||
|
|
||||||
|
type inheritanceOperation struct {
|
||||||
|
control *inheritanceControl
|
||||||
|
transfer *inheritanceTransfer
|
||||||
|
controlOrigin Origin
|
||||||
|
parent *document.Node
|
||||||
|
effective *document.Node
|
||||||
|
parentOrigins map[string]Origin
|
||||||
|
effectiveOrigins map[string]Origin
|
||||||
}
|
}
|
||||||
|
|
||||||
// A replacement retains the displaced source/derivation record while the
|
// A replacement retains the displaced source/derivation record while the
|
||||||
@@ -73,7 +84,7 @@ func expandSiblingDefinitionsWithLimits(scope *document.Node, scopePath Path, li
|
|||||||
return expandSiblingDefinitionsWithOrigins(scope, scopePath, limits, nil)
|
return expandSiblingDefinitionsWithOrigins(scope, scopePath, limits, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func expandSiblingDefinitionsWithOrigins(scope *document.Node, scopePath Path, limits projectionLimits, inputOrigins map[string]Origin) (*inheritanceScope, []Diagnostic) {
|
func expandSiblingDefinitionsWithOrigins(scope *document.Node, scopePath Path, limits projectionLimits, inputOrigins map[string]Origin, budget ...*retainedNodeCounter) (*inheritanceScope, []Diagnostic) {
|
||||||
if scope == nil || scope.Kind != document.Mapping {
|
if scope == nil || scope.Kind != document.Mapping {
|
||||||
return nil, []Diagnostic{{
|
return nil, []Diagnostic{{
|
||||||
Code: CodeInheritanceInvalid, Severity: SeverityError, Path: scopePath,
|
Code: CodeInheritanceInvalid, Severity: SeverityError, Path: scopePath,
|
||||||
@@ -93,6 +104,10 @@ func expandSiblingDefinitionsWithOrigins(scope *document.Node, scopePath Path, l
|
|||||||
resolvedOrigins := make(map[string]map[string]Origin, len(byName))
|
resolvedOrigins := make(map[string]map[string]Origin, len(byName))
|
||||||
state := make(map[string]uint8, len(byName))
|
state := make(map[string]uint8, len(byName))
|
||||||
result := &inheritanceScope{}
|
result := &inheritanceScope{}
|
||||||
|
counter := newRetainedNodeCounter(historyNodeLimit)
|
||||||
|
if len(budget) != 0 {
|
||||||
|
counter = budget[0]
|
||||||
|
}
|
||||||
retained := 1 // scope mapping node
|
retained := 1 // scope mapping node
|
||||||
for _, entry := range scope.Mapping {
|
for _, entry := range scope.Mapping {
|
||||||
if entry.Value.Kind == document.Mapping {
|
if entry.Value.Kind == document.Mapping {
|
||||||
@@ -159,7 +174,19 @@ func expandSiblingDefinitionsWithOrigins(scope *document.Node, scopePath Path, l
|
|||||||
}
|
}
|
||||||
|
|
||||||
payload := withoutInheritanceDirective(entry.Value)
|
payload := withoutInheritanceDirective(entry.Value)
|
||||||
|
operationStart, replacementStart := len(result.operations), len(result.replacements)
|
||||||
childOrigins := directInheritanceOrigins(payload, path, inputOrigins)
|
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 {
|
if limits.nodes >= 0 {
|
||||||
remaining := limits.nodes - retained - 1 // the sibling key
|
remaining := limits.nodes - retained - 1 // the sibling key
|
||||||
within := false
|
within := false
|
||||||
@@ -203,12 +230,26 @@ func expandSiblingDefinitionsWithOrigins(scope *document.Node, scopePath Path, l
|
|||||||
resolved[frame.name] = merged
|
resolved[frame.name] = merged
|
||||||
resolvedOrigins[frame.name] = mergedOrigins
|
resolvedOrigins[frame.name] = mergedOrigins
|
||||||
result.transfers = append(result.transfers, transfer)
|
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 {
|
} else {
|
||||||
resolved[frame.name] = document.Clone(payload)
|
resolved[frame.name] = document.Clone(payload)
|
||||||
resolvedOrigins[frame.name] = childOrigins
|
resolvedOrigins[frame.name] = childOrigins
|
||||||
}
|
}
|
||||||
count, _ := inheritanceLowerBoundCount(resolved[frame.name], limits.nodes)
|
count, _ := inheritanceLowerBoundCount(resolved[frame.name], limits.nodes)
|
||||||
retained += 1 + count
|
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
|
state[frame.name] = 2
|
||||||
stack = stack[:len(stack)-1]
|
stack = stack[:len(stack)-1]
|
||||||
}
|
}
|
||||||
|
|||||||
60
internal/engine/inheritance_explain.go
Normal file
60
internal/engine/inheritance_explain.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||||
|
|
||||||
|
func appendInheritanceExplanation(explanation *Explanation, working **document.Node, query Path, operations []inheritanceOperation, secrets map[string]bool) {
|
||||||
|
for _, operation := range operations {
|
||||||
|
changes := projectedInheritanceChanges(query, operation)
|
||||||
|
if operation.control != nil {
|
||||||
|
if !pathsIntersect(operation.control.path, query) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
before := valueSnapshot(*working, query, secrets)
|
||||||
|
replaceHistoryPath(working, operation.control.path, nil)
|
||||||
|
after := valueSnapshot(*working, query, secrets)
|
||||||
|
if len(changes) != 0 {
|
||||||
|
explanation.History = append(explanation.History, ValueChange{
|
||||||
|
Before: before, After: after, Origin: cloneOrigin(changes[0].Origin),
|
||||||
|
Dependencies: clonePaths(changes[0].Dependencies),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if operation.transfer == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
before := valueSnapshot(*working, query, secrets)
|
||||||
|
replaceHistoryPath(working, operation.transfer.to, operation.parent)
|
||||||
|
afterParent := valueSnapshot(*working, query, secrets)
|
||||||
|
parent, parentPresent := transferredOperationOrigin(operation, operationPathForQuery(operation.transfer.to, query))
|
||||||
|
if pathsIntersect(operation.transfer.to, query) && parentPresent {
|
||||||
|
parent.Operation = ChangeInheritance
|
||||||
|
parent.Inheritance = projectInheritanceSteps(parent)
|
||||||
|
explanation.History = append(explanation.History, ValueChange{
|
||||||
|
Before: before, After: afterParent, Origin: parent,
|
||||||
|
Dependencies: []Path{operation.transfer.from.clone(), operation.transfer.reference.clone()},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
replaceHistoryPath(working, operation.transfer.to, operation.effective)
|
||||||
|
if !pathsIntersect(operation.transfer.to, query) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
afterChild := valueSnapshot(*working, query, secrets)
|
||||||
|
if len(changes) != 0 {
|
||||||
|
last := changes[len(changes)-1]
|
||||||
|
if !parentPresent || len(changes) > 1 {
|
||||||
|
explanation.History = append(explanation.History, ValueChange{
|
||||||
|
Before: afterParent, After: afterChild, Origin: cloneOrigin(last.Origin),
|
||||||
|
Dependencies: clonePaths(last.Dependencies),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func operationPathForQuery(root, query Path) Path {
|
||||||
|
if pathContains(query, root) {
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
||||||
119
internal/engine/inheritance_history.go
Normal file
119
internal/engine/inheritance_history.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||||
|
|
||||||
|
// Retain source history at original paths. Structural operations are held once
|
||||||
|
// per relationship and projected onto a requested destination path only when
|
||||||
|
// History or Explain is queried.
|
||||||
|
func applyInheritanceMetadata(metadata *Metadata, raw *document.Node, rawOrigins map[string]Origin, overlays []overlayRecord, expansion *inheritanceScope) {
|
||||||
|
if metadata == nil || expansion == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sourceView := Metadata{
|
||||||
|
fields: make(map[string]FieldInfo),
|
||||||
|
history: metadata.history,
|
||||||
|
}
|
||||||
|
walkDocument(raw, Path{}, func(path Path, node *document.Node) {
|
||||||
|
origin, found := rawOrigins[path.String()]
|
||||||
|
if !found {
|
||||||
|
origin = Origin{Layer: LayerBase, Locations: []Location{publicLocation(node.Location)}}
|
||||||
|
}
|
||||||
|
sourceView.fields[path.String()] = FieldInfo{Explicit: true, Null: node.Kind == document.Null, Origin: cloneOrigin(origin)}
|
||||||
|
})
|
||||||
|
applyOverlayMetadata(&sourceView, overlays)
|
||||||
|
metadata.history = sourceView.history
|
||||||
|
metadata.inheritance = append([]inheritanceOperation(nil), expansion.operations...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func historyWithInheritance(path Path, stored []Change, operations []inheritanceOperation) []Change {
|
||||||
|
split := len(stored)
|
||||||
|
for index, change := range stored {
|
||||||
|
if change.Origin.Layer == LayerResolution {
|
||||||
|
split = index
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
history := cloneChanges(stored[:split])
|
||||||
|
for _, operation := range operations {
|
||||||
|
for _, change := range projectedInheritanceChanges(path, operation) {
|
||||||
|
if len(history) != 0 {
|
||||||
|
change.Previous = []Origin{cloneOrigin(history[len(history)-1].Origin)}
|
||||||
|
}
|
||||||
|
history = append(history, change)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, change := range stored[split:] {
|
||||||
|
cloned := cloneChange(change)
|
||||||
|
if len(history) != 0 {
|
||||||
|
cloned.Previous = []Origin{cloneOrigin(history[len(history)-1].Origin)}
|
||||||
|
}
|
||||||
|
history = append(history, cloned)
|
||||||
|
}
|
||||||
|
return history
|
||||||
|
}
|
||||||
|
|
||||||
|
func projectedInheritanceChanges(query Path, operation inheritanceOperation) []Change {
|
||||||
|
if operation.control != nil {
|
||||||
|
if !pathsIntersect(operation.control.path, query) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
origin := cloneOrigin(operation.controlOrigin)
|
||||||
|
origin.Operation = ChangeInheritance
|
||||||
|
origin.Inheritance = projectInheritanceSteps(origin)
|
||||||
|
return []Change{{Path: operation.control.path.clone(), Origin: origin,
|
||||||
|
Dependencies: []Path{operation.control.path.clone()}}}
|
||||||
|
}
|
||||||
|
if operation.transfer == nil || !pathsIntersect(operation.transfer.to, query) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
path := query
|
||||||
|
if pathContains(query, operation.transfer.to) {
|
||||||
|
path = operation.transfer.to
|
||||||
|
}
|
||||||
|
var changes []Change
|
||||||
|
parent, parentFound := transferredOperationOrigin(operation, path)
|
||||||
|
if parentFound {
|
||||||
|
parent.Operation = ChangeInheritance
|
||||||
|
parent.Inheritance = projectInheritanceSteps(parent)
|
||||||
|
changes = append(changes, Change{
|
||||||
|
Path: path.clone(), Origin: parent,
|
||||||
|
Dependencies: []Path{operation.transfer.from.clone(), operation.transfer.reference.clone()},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
effective, effectiveFound := operation.effectiveOrigins[path.String()]
|
||||||
|
if effectiveFound {
|
||||||
|
effective = cloneOrigin(effective)
|
||||||
|
effective.Operation = ChangeInheritance
|
||||||
|
}
|
||||||
|
if effectiveFound && (!parentFound || !originContainedBy(effective, parent)) {
|
||||||
|
effective.Inheritance = projectInheritanceSteps(effective)
|
||||||
|
changes = append(changes, Change{
|
||||||
|
Path: path.clone(), Origin: effective,
|
||||||
|
Dependencies: []Path{operation.transfer.reference.clone()},
|
||||||
|
})
|
||||||
|
} else if parentFound && !effectiveFound {
|
||||||
|
removed := cloneOrigin(operation.effectiveOrigins[operation.transfer.to.String()])
|
||||||
|
removed.Operation = ChangeInheritance
|
||||||
|
removed.Inheritance = projectInheritanceSteps(removed)
|
||||||
|
changes = append(changes, Change{
|
||||||
|
Path: operation.transfer.to.clone(), Origin: removed,
|
||||||
|
Dependencies: []Path{operation.transfer.reference.clone()},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return changes
|
||||||
|
}
|
||||||
|
|
||||||
|
func transferredOperationOrigin(operation inheritanceOperation, destination Path) (Origin, bool) {
|
||||||
|
relative, ok := relativeHistoryPath(operation.transfer.to, destination)
|
||||||
|
if !ok {
|
||||||
|
return Origin{}, false
|
||||||
|
}
|
||||||
|
source := appendInheritancePath(operation.transfer.from, relative)
|
||||||
|
origin, found := operation.parentOrigins[source.String()]
|
||||||
|
if !found {
|
||||||
|
return Origin{}, false
|
||||||
|
}
|
||||||
|
transferred := transferredInheritanceOrigins(map[string]Origin{source.String(): origin},
|
||||||
|
operation.transfer.from, operation.transfer.to, *operation.transfer)
|
||||||
|
return transferred[destination.String()], true
|
||||||
|
}
|
||||||
271
internal/engine/inheritance_history_test.go
Normal file
271
internal/engine/inheritance_history_test.go
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||||
|
)
|
||||||
|
|
||||||
|
func inheritanceInspectionForTest(t *testing.T, sources ...*document.Node) (*Inspection, *inheritanceScope) {
|
||||||
|
t.Helper()
|
||||||
|
composed, diagnostics := composeDocuments(sources...)
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
t.Fatal(diagnostics)
|
||||||
|
}
|
||||||
|
return inheritanceInspectionFromComposition(t, composed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func inheritanceInspectionFromComposition(t *testing.T, composed *composedDocument) (*Inspection, *inheritanceScope) {
|
||||||
|
t.Helper()
|
||||||
|
expanded, diagnostics := expandInheritanceWithOrigins(composed.root, composed.origins, defaultProjectionLimits())
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
t.Fatal(diagnostics)
|
||||||
|
}
|
||||||
|
baseline := &document.Node{Kind: document.Mapping}
|
||||||
|
presence := make(map[string]inputPresence)
|
||||||
|
collectPresence(expanded.root, Path{}, presence)
|
||||||
|
decoded := decodedConfiguration{
|
||||||
|
baseline: baseline, final: expanded.root, presence: presence, origins: expanded.origins,
|
||||||
|
}
|
||||||
|
metadata := metadataFromDecoded(decoded)
|
||||||
|
applyInheritanceMetadata(&metadata, composed.root, composed.origins, composed.overlays, expanded)
|
||||||
|
finalizeMetadataPaths(&metadata, expanded.root, composed.applications, baseline, composed.root)
|
||||||
|
return &Inspection{
|
||||||
|
metadata: metadata, baseline: baseline, input: expanded.root,
|
||||||
|
inputOrigins: expanded.origins, final: expanded.root,
|
||||||
|
applications: composed.applications, inheritance: expanded.operations,
|
||||||
|
}, expanded
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInheritanceHistoryFollowsFinalParentAfterSourceOverlay(t *testing.T) {
|
||||||
|
base, diagnostics := composeDocuments(parseCompositionDocument(t, "source-1", "parent: {value: original}\nchild: {extends: parent}\n"))
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
t.Fatal(diagnostics)
|
||||||
|
}
|
||||||
|
assembly := newAssemblyAccumulator(base)
|
||||||
|
if diagnostics := assembly.applyOverlay(parseCompositionDocument(t, "source-2", "parent: {value: latest}\n"), nil, nil, LayerOverlay); len(diagnostics) != 0 {
|
||||||
|
t.Fatal(diagnostics)
|
||||||
|
}
|
||||||
|
inspection, _ := inheritanceInspectionFromComposition(t, assembly.current)
|
||||||
|
path := Keys("child", "value")
|
||||||
|
explanation, found := inspection.Explain(path)
|
||||||
|
if !found || explanation.Effective.YAML != "latest\n" ||
|
||||||
|
len(explanation.History) == 0 || explanation.History[len(explanation.History)-1].Origin.Locations[0].SourceID != "source-2" {
|
||||||
|
t.Fatalf("overlaid parent contribution = %#v, %v", explanation, found)
|
||||||
|
}
|
||||||
|
parentHistory := inspection.Metadata().History(Keys("parent", "value"))
|
||||||
|
if len(parentHistory) < 2 || parentHistory[0].Origin.Layer != LayerBase ||
|
||||||
|
parentHistory[len(parentHistory)-1].Origin.Layer != LayerOverlay {
|
||||||
|
t.Fatalf("parent source history = %#v", parentHistory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInheritanceHistoryPreservesCancellationAndSupersededDirective(t *testing.T) {
|
||||||
|
base, diagnostics := composeDocuments(parseCompositionDocument(t, "source-1", "parent: {value: one}\nchild: {extends: missing, value: own}\n"))
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
t.Fatal(diagnostics)
|
||||||
|
}
|
||||||
|
assembly := newAssemblyAccumulator(base)
|
||||||
|
if diagnostics := assembly.applyOverlay(parseCompositionDocument(t, "source-2", "child: {extends: null}\n"), nil, nil, LayerOverlay); len(diagnostics) != 0 {
|
||||||
|
t.Fatal(diagnostics)
|
||||||
|
}
|
||||||
|
inspection, _ := inheritanceInspectionFromComposition(t, assembly.current)
|
||||||
|
control := Keys("child", inheritanceKey)
|
||||||
|
history := inspection.Metadata().History(control)
|
||||||
|
if len(history) < 3 || history[0].Origin.Locations[0].SourceID != "source-1" ||
|
||||||
|
history[1].Origin.Locations[0].SourceID != "source-2" ||
|
||||||
|
history[len(history)-1].Origin.Operation != ChangeInheritance {
|
||||||
|
t.Fatalf("superseded and cancelled directive history = %#v", history)
|
||||||
|
}
|
||||||
|
explanation, found := inspection.Explain(control)
|
||||||
|
if !found || explanation.Effective.Present || len(explanation.History) < 3 ||
|
||||||
|
explanation.History[len(explanation.History)-1].After.Present {
|
||||||
|
t.Fatalf("cancelled directive explanation = %#v, %v", explanation, found)
|
||||||
|
}
|
||||||
|
value, found := inspection.Explain(Keys("child", "value"))
|
||||||
|
if !found || value.Effective.YAML != "own\n" {
|
||||||
|
t.Fatalf("cancelled relationship changed own value = %#v, %v", value, found)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInheritanceHistoryProjectsNestedDestinationValues(t *testing.T) {
|
||||||
|
inspection, _ := inheritanceInspectionForTest(t, parseCompositionDocument(t, "source-1", `base:
|
||||||
|
settings:
|
||||||
|
primary: {value: old}
|
||||||
|
secondary: {extends: primary}
|
||||||
|
custom:
|
||||||
|
extends: base
|
||||||
|
settings:
|
||||||
|
primary: {value: new}
|
||||||
|
`))
|
||||||
|
path := Keys("custom", "settings", "secondary", "value")
|
||||||
|
history := inspection.Metadata().History(path)
|
||||||
|
structural := 0
|
||||||
|
for _, change := range history {
|
||||||
|
if change.Origin.Operation == ChangeInheritance {
|
||||||
|
structural++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if structural == 0 {
|
||||||
|
t.Fatalf("nested destination history = %#v", history)
|
||||||
|
}
|
||||||
|
explanation, found := inspection.Explain(path)
|
||||||
|
if !found || explanation.Effective.YAML != "new\n" ||
|
||||||
|
len(explanation.Origin.Inheritance) == 0 ||
|
||||||
|
explanation.Origin.Inheritance[len(explanation.Origin.Inheritance)-1].To.String() != path.String() {
|
||||||
|
t.Fatalf("nested destination explanation = %#v, %v", explanation, found)
|
||||||
|
}
|
||||||
|
for _, change := range explanation.History {
|
||||||
|
if change.Origin.Operation == ChangeDecoding {
|
||||||
|
t.Fatalf("nested inheritance was attributed to decoding: %#v", explanation.History)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInheritanceHistoryShowsParentAndEqualChildContribution(t *testing.T) {
|
||||||
|
inspection, _ := inheritanceInspectionForTest(t,
|
||||||
|
parseCompositionDocument(t, "source-1", "parent: {value: same}\n"),
|
||||||
|
parseCompositionDocument(t, "source-2", "child: {extends: parent, value: same}\n"))
|
||||||
|
path := Keys("child", "value")
|
||||||
|
history := inspection.Metadata().History(path)
|
||||||
|
var structural []Change
|
||||||
|
for _, change := range history {
|
||||||
|
if change.Origin.Operation == ChangeInheritance {
|
||||||
|
structural = append(structural, change)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(structural) != 2 || structural[0].Origin.Locations[0].SourceID != "source-1" ||
|
||||||
|
structural[1].Origin.Locations[0].SourceID != "source-2" ||
|
||||||
|
len(structural[0].Dependencies) != 2 || structural[0].Dependencies[0].String() != "/parent" ||
|
||||||
|
len(structural[0].Origin.Inheritance) != 1 {
|
||||||
|
t.Fatalf("equal-valued structural history = %#v", history)
|
||||||
|
}
|
||||||
|
explanation, found := inspection.Explain(path)
|
||||||
|
if !found || explanation.Effective.YAML != "same\n" {
|
||||||
|
t.Fatalf("explanation = %#v, %v", explanation, found)
|
||||||
|
}
|
||||||
|
var equalEvents int
|
||||||
|
for _, change := range explanation.History {
|
||||||
|
if change.Origin.Operation == ChangeDecoding {
|
||||||
|
t.Fatalf("inheritance was attributed to decoding: %#v", explanation.History)
|
||||||
|
}
|
||||||
|
if change.Origin.Operation == ChangeInheritance && change.Before.YAML == change.After.YAML {
|
||||||
|
equalEvents++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if equalEvents != 2 {
|
||||||
|
t.Fatalf("equal parent and child operations = %#v", explanation.History)
|
||||||
|
}
|
||||||
|
parentHistory := inspection.Metadata().History(Keys("parent", "value"))
|
||||||
|
for _, change := range parentHistory {
|
||||||
|
if change.Origin.Operation == ChangeInheritance {
|
||||||
|
t.Fatalf("source definition acquired a destination operation: %#v", parentHistory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInheritanceHistoryRetainsControlsAndDisplacedPaths(t *testing.T) {
|
||||||
|
inspection, _ := inheritanceInspectionForTest(t,
|
||||||
|
parseCompositionDocument(t, "source-1", "parent: {branch: {detail: inherited}}\n"),
|
||||||
|
parseCompositionDocument(t, "source-2", "child: {extends: parent, branch: replaced}\n"))
|
||||||
|
control := Keys("child", inheritanceKey)
|
||||||
|
controlHistory := inspection.Metadata().History(control)
|
||||||
|
if len(controlHistory) < 2 || controlHistory[len(controlHistory)-1].Origin.Operation != ChangeInheritance {
|
||||||
|
t.Fatalf("control history = %#v", controlHistory)
|
||||||
|
}
|
||||||
|
controlExplanation, found := inspection.Explain(control)
|
||||||
|
if !found || controlExplanation.Effective.Present ||
|
||||||
|
controlExplanation.History[len(controlExplanation.History)-1].After.Present {
|
||||||
|
t.Fatalf("consumed control = %#v, %v", controlExplanation, found)
|
||||||
|
}
|
||||||
|
path := Keys("child", "branch", "detail")
|
||||||
|
if _, found := inspection.Metadata().Lookup(path); found {
|
||||||
|
t.Fatal("displaced descendant remained effective")
|
||||||
|
}
|
||||||
|
listed := false
|
||||||
|
for _, historical := range inspection.Metadata().HistoricalPaths() {
|
||||||
|
if historical.String() == path.String() {
|
||||||
|
listed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !listed {
|
||||||
|
t.Fatal("displaced inherited descendant was not enumerable")
|
||||||
|
}
|
||||||
|
history := inspection.Metadata().History(path)
|
||||||
|
if len(history) < 2 || history[len(history)-1].Origin.Operation != ChangeInheritance {
|
||||||
|
t.Fatalf("displaced descendant history = %#v", history)
|
||||||
|
}
|
||||||
|
explanation, found := inspection.Explain(path)
|
||||||
|
if !found || explanation.Effective.Present {
|
||||||
|
t.Fatalf("displaced descendant explanation = %#v, %v", explanation, found)
|
||||||
|
}
|
||||||
|
var sawInherited, sawRemoval bool
|
||||||
|
for _, change := range explanation.History {
|
||||||
|
if change.Origin.Operation != ChangeInheritance {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sawInherited = sawInherited || change.After.Present
|
||||||
|
sawRemoval = sawRemoval || (change.Before.Present && !change.After.Present)
|
||||||
|
}
|
||||||
|
if !sawInherited || !sawRemoval {
|
||||||
|
t.Fatalf("displaced value sequence = %#v", explanation.History)
|
||||||
|
}
|
||||||
|
var rendered bytes.Buffer
|
||||||
|
if err := inspection.WriteExplanation(&rendered, path); err != nil ||
|
||||||
|
!strings.Contains(rendered.String(), "structural inheritance") {
|
||||||
|
t.Fatalf("rendered structural explanation = %q, %v", rendered.String(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInheritanceHistoryPrecedesApplicationResolution(t *testing.T) {
|
||||||
|
inspection, _ := inheritanceInspectionForTest(t,
|
||||||
|
parseCompositionDocument(t, "source-1", "parent: {value: original}\nchild: {extends: parent}\n"))
|
||||||
|
path := Keys("child", "value")
|
||||||
|
replacement := &document.Node{Kind: document.String, Text: "derived"}
|
||||||
|
snapshot := resolutionSnapshot{
|
||||||
|
change: Change{Path: path, Origin: Origin{Layer: LayerResolution, Operation: ChangeNormalization}},
|
||||||
|
after: replacement,
|
||||||
|
}
|
||||||
|
final := document.Clone(inspection.final)
|
||||||
|
replaceHistoryPath(&final, path, replacement)
|
||||||
|
inspection.final = final
|
||||||
|
inspection.resolution = []resolutionSnapshot{snapshot}
|
||||||
|
inspection.metadata.history[path.String()] = append(inspection.metadata.history[path.String()], snapshot.change)
|
||||||
|
inspection.metadata.fields[path.String()] = FieldInfo{Explicit: true, Origin: snapshot.change.Origin}
|
||||||
|
history := inspection.Metadata().History(path)
|
||||||
|
if len(history) < 2 || history[len(history)-2].Origin.Operation != ChangeInheritance ||
|
||||||
|
history[len(history)-1].Origin.Operation != ChangeNormalization {
|
||||||
|
t.Fatalf("resolution order = %#v", history)
|
||||||
|
}
|
||||||
|
explanation, found := inspection.Explain(path)
|
||||||
|
if !found || explanation.Effective.YAML != "derived\n" ||
|
||||||
|
explanation.History[len(explanation.History)-1].Origin.Operation != ChangeNormalization {
|
||||||
|
t.Fatalf("resolved explanation = %#v, %v", explanation, found)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHistoryNodeBudgetCountsSharedInheritanceEvidenceOnce(t *testing.T) {
|
||||||
|
shared := &document.Node{Kind: document.String, Text: "shared"}
|
||||||
|
extra := &document.Node{Kind: document.String, Text: "extra"}
|
||||||
|
expansion := &inheritanceScope{
|
||||||
|
root: shared,
|
||||||
|
operations: []inheritanceOperation{{parent: shared, effective: shared}},
|
||||||
|
replacements: []inheritanceReplacement{{before: shared, after: extra}},
|
||||||
|
}
|
||||||
|
if !historyNodesWithin(2, nil, nil, nil, nil, nil, expansion) ||
|
||||||
|
historyNodesWithin(1, nil, nil, nil, nil, nil, expansion) {
|
||||||
|
t.Fatal("shared evidence node accounting is incorrect")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInheritanceExpansionEnforcesRetainedHistoryBudget(t *testing.T) {
|
||||||
|
input := parseCompositionDocument(t, "source-1", "parent: {value: original}\nchild: {extends: parent, value: replacement}\n")
|
||||||
|
if _, diagnostics := expandInheritanceWithBudgets(input, nil, defaultProjectionLimits(), 2); len(diagnostics) != 1 || diagnostics[0].Code != CodeExplainLimitExceeded {
|
||||||
|
t.Fatalf("retained history limit = %#v", diagnostics)
|
||||||
|
}
|
||||||
|
if _, diagnostics := expandInheritanceWithBudgets(input, nil, defaultProjectionLimits(), 100); len(diagnostics) != 0 {
|
||||||
|
t.Fatalf("sufficient retained history budget = %#v", diagnostics)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,9 +11,11 @@ type inheritanceWalker struct {
|
|||||||
root **document.Node
|
root **document.Node
|
||||||
origins map[string]Origin
|
origins map[string]Origin
|
||||||
limits projectionLimits
|
limits projectionLimits
|
||||||
|
retained *retainedNodeCounter
|
||||||
transfers []inheritanceTransfer
|
transfers []inheritanceTransfer
|
||||||
controls []inheritanceControl
|
controls []inheritanceControl
|
||||||
replacements []inheritanceReplacement
|
replacements []inheritanceReplacement
|
||||||
|
operations []inheritanceOperation
|
||||||
}
|
}
|
||||||
|
|
||||||
// expandInheritanceTree resolves each destination scope after its enclosing
|
// expandInheritanceTree resolves each destination scope after its enclosing
|
||||||
@@ -24,12 +26,19 @@ func expandInheritanceTree(input *document.Node, limits projectionLimits) (*inhe
|
|||||||
}
|
}
|
||||||
|
|
||||||
func expandInheritanceWithOrigins(input *document.Node, inputOrigins map[string]Origin, limits projectionLimits) (*inheritanceScope, []Diagnostic) {
|
func expandInheritanceWithOrigins(input *document.Node, inputOrigins map[string]Origin, limits projectionLimits) (*inheritanceScope, []Diagnostic) {
|
||||||
|
return expandInheritanceWithBudgets(input, inputOrigins, limits, historyNodeLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandInheritanceWithBudgets(input *document.Node, inputOrigins map[string]Origin, limits projectionLimits, historyLimit int) (*inheritanceScope, []Diagnostic) {
|
||||||
if input == nil || input.Kind != document.Mapping {
|
if input == nil || input.Kind != document.Mapping {
|
||||||
return nil, []Diagnostic{{Code: CodeInheritanceInvalid, Severity: SeverityError,
|
return nil, []Diagnostic{{Code: CodeInheritanceInvalid, Severity: SeverityError,
|
||||||
Message: "inheritance requires a mapping document root"}}
|
Message: "inheritance requires a mapping document root"}}
|
||||||
}
|
}
|
||||||
root := input
|
root := input
|
||||||
walker := inheritanceWalker{root: &root, limits: limits, origins: cloneOriginIndex(inputOrigins)}
|
walker := inheritanceWalker{
|
||||||
|
root: &root, limits: limits, origins: cloneOriginIndex(inputOrigins),
|
||||||
|
retained: newRetainedNodeCounter(historyLimit),
|
||||||
|
}
|
||||||
if problem := walker.visit(&root, Path{}, true); problem != nil {
|
if problem := walker.visit(&root, Path{}, true); problem != nil {
|
||||||
return nil, []Diagnostic{*problem}
|
return nil, []Diagnostic{*problem}
|
||||||
}
|
}
|
||||||
@@ -38,7 +47,7 @@ func expandInheritanceWithOrigins(input *document.Node, inputOrigins map[string]
|
|||||||
}
|
}
|
||||||
return &inheritanceScope{
|
return &inheritanceScope{
|
||||||
root: root, origins: walker.origins, transfers: walker.transfers,
|
root: root, origins: walker.origins, transfers: walker.transfers,
|
||||||
controls: walker.controls, replacements: walker.replacements,
|
controls: walker.controls, replacements: walker.replacements, operations: walker.operations,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,11 +68,14 @@ func (w *inheritanceWalker) visit(node **document.Node, path Path, unscoped bool
|
|||||||
w.controls = append(w.controls, inheritanceControl{
|
w.controls = append(w.controls, inheritanceControl{
|
||||||
path: referencePath, locations: []Location{location}, cancelled: true,
|
path: referencePath, locations: []Location{location}, cancelled: true,
|
||||||
})
|
})
|
||||||
|
origin := directInheritanceOrigins(reference.Value, referencePath, w.origins)[referencePath.String()]
|
||||||
|
control := inheritanceControl{path: referencePath, locations: []Location{location}, cancelled: true}
|
||||||
|
w.operations = append(w.operations, inheritanceOperation{control: &control, controlOrigin: origin})
|
||||||
deleteOriginSubtree(w.origins, referencePath, reference.Value)
|
deleteOriginSubtree(w.origins, referencePath, reference.Value)
|
||||||
*node = withoutInheritanceDirective(*node)
|
*node = withoutInheritanceDirective(*node)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resolved, diagnostics := expandSiblingDefinitionsWithOrigins(*node, path, w.limits, w.origins)
|
resolved, diagnostics := expandSiblingDefinitionsWithOrigins(*node, path, w.limits, w.origins, w.retained)
|
||||||
if len(diagnostics) != 0 {
|
if len(diagnostics) != 0 {
|
||||||
return &diagnostics[0]
|
return &diagnostics[0]
|
||||||
}
|
}
|
||||||
@@ -75,6 +87,7 @@ func (w *inheritanceWalker) visit(node **document.Node, path Path, unscoped bool
|
|||||||
w.transfers = append(w.transfers, resolved.transfers...)
|
w.transfers = append(w.transfers, resolved.transfers...)
|
||||||
w.controls = append(w.controls, resolved.controls...)
|
w.controls = append(w.controls, resolved.controls...)
|
||||||
w.replacements = append(w.replacements, resolved.replacements...)
|
w.replacements = append(w.replacements, resolved.replacements...)
|
||||||
|
w.operations = append(w.operations, resolved.operations...)
|
||||||
if !inheritanceLowerBoundWithin(*w.root, w.limits.nodes) {
|
if !inheritanceLowerBoundWithin(*w.root, w.limits.nodes) {
|
||||||
problem := inheritanceBudgetDiagnostic(path, (*node).Location)
|
problem := inheritanceBudgetDiagnostic(path, (*node).Location)
|
||||||
return &problem
|
return &problem
|
||||||
@@ -251,3 +264,11 @@ func inheritanceBudgetDiagnostic(path Path, location document.Location) Diagnost
|
|||||||
Message: "expanded configuration exceeds the projection limit", Primary: &primary,
|
Message: "expanded configuration exceeds the projection limit", Primary: &primary,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func inheritanceHistoryBudgetDiagnostic(path Path, location document.Location) Diagnostic {
|
||||||
|
primary := publicLocation(location)
|
||||||
|
return Diagnostic{
|
||||||
|
Code: CodeExplainLimitExceeded, Severity: SeverityError, Path: path,
|
||||||
|
Message: "retained inheritance history exceeds the node limit", Primary: &primary,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ type Inspection struct {
|
|||||||
final *document.Node
|
final *document.Node
|
||||||
resolution []resolutionSnapshot
|
resolution []resolutionSnapshot
|
||||||
applications []historyApplication
|
applications []historyApplication
|
||||||
|
inheritance []inheritanceOperation
|
||||||
secrets map[string]bool
|
secrets map[string]bool
|
||||||
secretReferences map[string]string
|
secretReferences map[string]string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -254,8 +254,9 @@ func cloneMetadata(metadata Metadata) Metadata {
|
|||||||
return Metadata{}
|
return Metadata{}
|
||||||
}
|
}
|
||||||
cloned := Metadata{
|
cloned := Metadata{
|
||||||
fields: make(map[string]FieldInfo, len(metadata.fields)),
|
fields: make(map[string]FieldInfo, len(metadata.fields)),
|
||||||
history: make(map[string][]Change, len(metadata.history)),
|
history: make(map[string][]Change, len(metadata.history)),
|
||||||
|
inheritance: append([]inheritanceOperation(nil), metadata.inheritance...),
|
||||||
}
|
}
|
||||||
cloned.effectivePaths = make(map[string]Path, len(metadata.effectivePaths))
|
cloned.effectivePaths = make(map[string]Path, len(metadata.effectivePaths))
|
||||||
for pointer, path := range metadata.effectivePaths {
|
for pointer, path := range metadata.effectivePaths {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type Metadata struct {
|
|||||||
historicalPaths map[string][]Path
|
historicalPaths map[string][]Path
|
||||||
fields map[string]FieldInfo
|
fields map[string]FieldInfo
|
||||||
history map[string][]Change
|
history map[string][]Change
|
||||||
|
inheritance []inheritanceOperation
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lookup returns information for an effective path. Unknown and invalid paths
|
// Lookup returns information for an effective path. Unknown and invalid paths
|
||||||
@@ -25,7 +26,9 @@ func (m Metadata) Lookup(path Path) (FieldInfo, bool) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return FieldInfo{}, false
|
return FieldInfo{}, false
|
||||||
}
|
}
|
||||||
return cloneFieldInfo(info), true
|
detached := cloneFieldInfo(info)
|
||||||
|
detached.Origin.Inheritance = projectInheritanceSteps(detached.Origin)
|
||||||
|
return detached, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsExplicit reports whether a surviving input contribution explicitly defined
|
// IsExplicit reports whether a surviving input contribution explicitly defined
|
||||||
@@ -43,15 +46,8 @@ func (m Metadata) History(path Path) []Change {
|
|||||||
if !m.hasHistoricalPath(path) {
|
if !m.hasHistoricalPath(path) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
history, ok := m.history[path.String()]
|
history := m.history[path.String()]
|
||||||
if !ok {
|
return historyWithInheritance(path, history, m.inheritance)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
cloned := make([]Change, len(history))
|
|
||||||
for i, change := range history {
|
|
||||||
cloned[i] = cloneChange(change)
|
|
||||||
}
|
|
||||||
return cloned
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HistoricalPaths returns the paths observed during loading, including effective
|
// HistoricalPaths returns the paths observed during loading, including effective
|
||||||
@@ -136,6 +132,16 @@ func finalizeMetadataPaths(m *Metadata, final *document.Node, applications []his
|
|||||||
}
|
}
|
||||||
walkDocument(application.value, prefix, add)
|
walkDocument(application.value, prefix, add)
|
||||||
}
|
}
|
||||||
|
for _, operation := range m.inheritance {
|
||||||
|
if operation.control != nil {
|
||||||
|
add(operation.control.path, nil)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if operation.transfer != nil {
|
||||||
|
walkDocument(operation.parent, operation.transfer.to, add)
|
||||||
|
walkDocument(operation.effective, operation.transfer.to, add)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// effectivePathIndex records document-derived paths with their contextual segment
|
// effectivePathIndex records document-derived paths with their contextual segment
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ type ChangeKind string
|
|||||||
const (
|
const (
|
||||||
// ChangeDecoding records the built-in transition to canonical typed values.
|
// ChangeDecoding records the built-in transition to canonical typed values.
|
||||||
// It is inspection metadata, not an operation accepted by Resolution.Record.
|
// It is inspection metadata, not an operation accepted by Resolution.Record.
|
||||||
ChangeDecoding ChangeKind = "decoding"
|
ChangeDecoding ChangeKind = "decoding"
|
||||||
|
// ChangeInheritance records structural input expansion. It is inspection
|
||||||
|
// metadata, not an operation accepted by Resolution.Record.
|
||||||
|
ChangeInheritance ChangeKind = "inheritance"
|
||||||
ChangeDerivedDefault ChangeKind = "derived_default"
|
ChangeDerivedDefault ChangeKind = "derived_default"
|
||||||
ChangeNormalization ChangeKind = "normalization"
|
ChangeNormalization ChangeKind = "normalization"
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user