Replay added configuration paths in explanations

This commit is contained in:
2026-09-06 19:08:29 -05:00
parent 831c83a0b5
commit edd82e15b2
4 changed files with 99 additions and 3 deletions

View File

@@ -162,7 +162,10 @@ parser values. `Present=false` with empty YAML means absence or deletion; explic
null is present and renders as `null`. The history starts with the canonical typed
application-default or Go-zero baseline, then includes only relevant base sources in
traversal order, the selected profile, supplied CLI records, and audited net
resolution operations. Explicit equal writes remain visible. When input replay
resolution operations. Overlapping parent/child resolution records are rendered
as one net subtree event with combined reasons and dependencies, rather than
pretending that the recorded assignments ran in lexical order. Explicit equal
writes remain visible. When input replay
differs from the final typed input, a `ChangeDecoding` event records the transition
before domain resolution. Its origin retains the decoded path's source/default
attribution; its reason identifies static-default retention and canonical decoding.

View File

@@ -132,6 +132,11 @@ the effective value and origin without implying that a final-present path is abs
## 5. P2 — Replay newly created resolution paths
Status: complete. Replay inserts new mapping entries/subtrees and coalesces
overlapping parent/child net operations while retaining reasons and dependencies.
Leaf/container regressions cover nested creation, removal and overlapping records.
Full tests pass with the issue-8 TMPDIR workaround; vet, build and whitespace pass.
Evidence: [explain.go](../../explain.go), `replaceHistoryPath`, returns immediately
when a mapping entry is absent, even when the replacement is a newly created value.

View File

@@ -134,7 +134,7 @@ func (r *Result[T]) Explain(path Path) (Explanation, bool) {
}
working = document.Clone(r.input)
for _, snapshot := range r.resolution {
for _, snapshot := range coalescedResolutionSnapshots(r.resolution) {
relevant := pathsIntersect(snapshot.change.Path, path)
var before ValueSnapshot
if relevant {
@@ -271,6 +271,37 @@ func mergeHistoryAtPath(root **document.Node, path Path, incoming *document.Node
}
}
// Resolution records describe one before/final transformation, not assignment
// order. A parent already carries its descendants' final values; merge overlapping
// intent into that event rather than replay a child assignment a second time.
func coalescedResolutionSnapshots(snapshots []resolutionSnapshot) []resolutionSnapshot {
groups := make([]resolutionSnapshot, 0, len(snapshots))
for _, snapshot := range snapshots {
merged := false
for i := range groups {
if !pathContains(groups[i].change.Path, snapshot.change.Path) {
continue
}
for _, reason := range snapshot.change.Reasons {
groups[i].change.Reasons = appendUniqueString(groups[i].change.Reasons, reason)
}
for _, dependency := range snapshot.change.Dependencies {
groups[i].change.Dependencies = appendUniquePath(groups[i].change.Dependencies, dependency)
}
if snapshot.change.Origin.Operation == ChangeNormalization {
groups[i].change.Origin.Operation = ChangeNormalization
}
merged = true
break
}
if !merged {
snapshot.change = cloneChange(snapshot.change)
groups = append(groups, snapshot)
}
}
return groups
}
func replaceHistoryPath(root **document.Node, path Path, replacement *document.Node) {
if root == nil || *root == nil {
return
@@ -289,7 +320,19 @@ func replaceHistoryPath(root **document.Node, path Path, replacement *document.N
}
position := documentEntryIndex(current.Mapping, segment.key)
if position < 0 {
return
if replacement == nil {
return
}
child := &document.Node{Kind: document.Mapping}
if last {
child = document.Clone(replacement)
}
current.Mapping = append(current.Mapping, document.Entry{Key: segment.key, Value: child})
if last {
return
}
current = child
continue
}
if last {
if replacement == nil {

View File

@@ -365,3 +365,48 @@ func TestExplanationReconcilesDefaultsAfterInputReplacement(t *testing.T) {
}
}
}
func TestExplanationReplaysResolutionAdditionsAndOverlappingIntent(t *testing.T) {
type cfg struct {
M map[string]any `yaml:"m"`
}
for _, overlap := range []bool{false, true} {
t.Run(fmt.Sprint(overlap), func(t *testing.T) {
result, err := Load(Bytes("base", []byte("m: {old: 1}\n")), Options[cfg]{Definition: Definition[cfg]{Resolve: func(c *cfg, r *Resolution) error {
c.M["new"] = map[string]any{"child": int64(7)}
delete(c.M, "old")
r.Record(Keys("m", "new"), ChangeDerivedDefault, "add nested", Keys("m", "old"))
r.Record(Keys("m", "old"), ChangeNormalization, "remove old")
if overlap {
r.Record(Keys("m"), ChangeNormalization, "update container")
}
return nil
}}})
if err != nil {
t.Fatal(err)
}
for _, path := range []Path{Keys("m"), Keys("m", "new"), Keys("m", "new", "child"), Keys("m", "old")} {
x, ok := result.Explain(path)
if !ok {
t.Fatal("missing known history")
}
var count int
for _, v := range x.History {
if v.Origin.Layer == LayerResolution {
count++
if overlap && len(v.Reasons) != 3 {
t.Fatal("lost overlapping intent")
}
}
}
if overlap && count != 1 {
t.Fatalf("overlapping records applied %d times", count)
}
last := x.History[len(x.History)-1]
if last.After != x.Effective {
t.Fatalf("incorrect replay for %s: %#v", path, x)
}
}
})
}
}