Prepare coalesced resolution replay

This commit is contained in:
2026-10-09 03:49:43 +00:00
parent 5b8b7a2415
commit 5fedc11c8f
8 changed files with 138 additions and 14 deletions

View File

@@ -331,7 +331,7 @@ Roadmap candidate 4. Owner: engine `override.go`.
Validate: `go test ./internal/engine -run 'Override|CLI|Provenance|Diagnostic'`
and the everyday check. No policy changes.
## Stage 13: Coalesce resolution replay once with indexed ancestors
## Stage 13: Coalesce resolution replay once with indexed ancestors — Complete
Roadmap candidate 7. Owners: engine `history.go`, `load.go`, `load_result.go`.

View File

@@ -91,7 +91,7 @@ func (r *Inspection) Explain(path Path) (Explanation, bool) {
}
working = document.Clone(r.input)
for _, snapshot := range coalescedResolutionSnapshots(r.resolution) {
for _, snapshot := range r.resolutionReplay {
relevant := pathsIntersect(snapshot.change.Path, path)
var before ValueSnapshot
if relevant {

View File

@@ -520,3 +520,49 @@ func TestExplanationReplaysResolutionAdditionsAndOverlappingIntent(t *testing.T)
})
}
}
func TestExplanationReplayDescriptorsRemainDetachedAcrossQueries(t *testing.T) {
type config struct {
Count int `yaml:"count"`
}
result, err := testLoad(Bytes("resolution-explanation.yml", []byte("count: 1\n")), Options[config]{
Definition: Definition[config]{Resolve: func(value *config, resolution *Resolution) error {
value.Count = 2
resolution.Record(Keys("count"), ChangeNormalization, "normalized", Keys("count"))
return nil
}},
})
if err != nil {
t.Fatal(err)
}
first, ok := result.Explain(Keys("count"))
if !ok {
t.Fatal("missing explanation")
}
var firstResolution *ValueChange
for index := range first.History {
if first.History[index].Origin.Layer == LayerResolution {
firstResolution = &first.History[index]
break
}
}
if firstResolution == nil || len(firstResolution.Reasons) != 1 || len(firstResolution.Dependencies) != 1 {
t.Fatalf("resolution history = %#v", first.History)
}
firstResolution.Reasons[0] = "mutated"
firstResolution.Dependencies[0].segments[0].key = "mutated"
second, ok := result.Explain(Keys("count"))
if !ok {
t.Fatal("missing repeated explanation")
}
for _, change := range second.History {
if change.Origin.Layer == LayerResolution {
if !reflect.DeepEqual(change.Reasons, []string{"normalized"}) || len(change.Dependencies) != 1 || change.Dependencies[0].String() != "/count" {
t.Fatalf("repeated explanation observed caller mutation: %#v", change)
}
return
}
}
t.Fatalf("resolution change missing from repeated explanation: %#v", second.History)
}

View File

@@ -179,28 +179,50 @@ func mergeHistoryAtPath(root **document.Node, path Path, incoming *document.Node
// intent into that event rather than replay a child assignment a second time.
func coalescedResolutionSnapshots(snapshots []resolutionSnapshot) []resolutionSnapshot {
groups := make([]resolutionSnapshot, 0, len(snapshots))
reasons := make([]map[string]struct{}, 0, len(snapshots))
dependencies := make([]map[string]struct{}, 0, len(snapshots))
var coverage pathPrefixIndex
for _, snapshot := range snapshots {
merged := false
for i := range groups {
if !pathContains(groups[i].change.Path, snapshot.change.Path) {
continue
pointer := snapshot.change.Path.String()
groupIndex := -1
coverage.visitAncestors(pointer, func(id int) {
if groupIndex < 0 || id < groupIndex {
groupIndex = id
}
})
if groupIndex >= 0 {
group := &groups[groupIndex]
for _, reason := range snapshot.change.Reasons {
groups[i].change.Reasons = appendUniqueString(groups[i].change.Reasons, reason)
if _, found := reasons[groupIndex][reason]; !found {
reasons[groupIndex][reason] = struct{}{}
group.change.Reasons = append(group.change.Reasons, reason)
}
}
for _, dependency := range snapshot.change.Dependencies {
groups[i].change.Dependencies = appendUniquePath(groups[i].change.Dependencies, dependency)
dependencyPointer := dependency.String()
if _, found := dependencies[groupIndex][dependencyPointer]; !found {
dependencies[groupIndex][dependencyPointer] = struct{}{}
group.change.Dependencies = append(group.change.Dependencies, dependency.clone())
}
}
if snapshot.change.Origin.Operation == ChangeNormalization {
groups[i].change.Origin.Operation = ChangeNormalization
group.change.Origin.Operation = ChangeNormalization
}
merged = true
break
continue
}
if !merged {
snapshot.change = cloneChange(snapshot.change)
groups = append(groups, snapshot)
snapshot.change = cloneChange(snapshot.change)
groups = append(groups, snapshot)
reasonSet := make(map[string]struct{}, len(snapshot.change.Reasons))
for _, reason := range snapshot.change.Reasons {
reasonSet[reason] = struct{}{}
}
reasons = append(reasons, reasonSet)
dependencySet := make(map[string]struct{}, len(snapshot.change.Dependencies))
for _, dependency := range snapshot.change.Dependencies {
dependencySet[dependency.String()] = struct{}{}
}
dependencies = append(dependencies, dependencySet)
coverage.insert(pointer, len(groups)-1)
}
return groups
}

View File

@@ -324,6 +324,7 @@ func TestInheritanceHistoryPrecedesApplicationResolution(t *testing.T) {
replaceHistoryPath(&final, path, replacement)
inspection.final = final
inspection.resolution = []resolutionSnapshot{snapshot}
inspection.resolutionReplay = coalescedResolutionSnapshots(inspection.resolution)
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)

View File

@@ -85,6 +85,7 @@ type Inspection struct {
inputOrigins map[string]Origin
final *document.Node
resolution []resolutionSnapshot
resolutionReplay []resolutionSnapshot
applications []historyApplication
inheritance []inheritanceOperation
secrets map[string]bool

View File

@@ -108,6 +108,7 @@ func publishLoadResult[T any](schema *schemaDescriptor, definition Definition[T]
inputOrigins: cloneOriginIndex(decoded.origins),
final: final,
resolution: resolutionSnapshots,
resolutionReplay: coalescedResolutionSnapshots(resolutionSnapshots),
applications: applications,
inheritance: inheritanceOperations(resolved.inheritance),
secrets: secrets,

View File

@@ -1,11 +1,64 @@
package engine
import (
"reflect"
"testing"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
)
func TestCoalescedResolutionSnapshotsMatchOrderedScan(t *testing.T) {
parsedDependency := parseOverrideTestPath(t, "/dependencies/item")
snapshots := []resolutionSnapshot{
{change: Change{Path: Keys("a", "b"), Reasons: []string{"child", "shared", "child"}, Dependencies: []Path{Keys("dependencies", "item")}, Origin: Origin{Operation: ChangeDerivedDefault}}},
{change: Change{Path: Keys("a", "c"), Reasons: []string{"other"}, Dependencies: []Path{Keys("other")}, Origin: Origin{Operation: ChangeDerivedDefault}}},
{change: Change{Path: Keys("a"), Reasons: []string{"parent"}, Dependencies: []Path{Keys("parent")}, Origin: Origin{Operation: ChangeDerivedDefault}}},
{change: Change{Path: Keys("a", "b", "grand"), Reasons: []string{"deep", "shared"}, Dependencies: []Path{parsedDependency, Keys("deep")}, Origin: Origin{Operation: ChangeDerivedDefault}}},
{change: Change{Path: Path{}, Reasons: []string{"root"}, Origin: Origin{Operation: ChangeDerivedDefault}}},
{change: Change{Path: Keys("outside"), Reasons: []string{"root child"}, Origin: Origin{Operation: ChangeDerivedDefault}}},
{change: Change{Path: Keys("a", "b", "normalization"), Reasons: []string{"normalized"}, Origin: Origin{Operation: ChangeNormalization}}},
{change: Change{Path: Keys("a", "none"), Reasons: []string{"parent child"}, Origin: Origin{Operation: ChangeDerivedDefault}}},
}
want := coalescedResolutionSnapshotsByScan(snapshots)
got := coalescedResolutionSnapshots(snapshots)
if !reflect.DeepEqual(got, want) {
t.Fatalf("coalesced snapshots differ from ordered scan:\n got: %#v\nwant: %#v", got, want)
}
if len(got) != 4 || got[0].change.Origin.Operation != ChangeNormalization ||
!reflect.DeepEqual(got[0].change.Reasons, []string{"child", "shared", "child", "deep", "normalized"}) ||
len(got[0].change.Dependencies) != 2 || got[3].change.Path.String() != "" {
t.Fatalf("coalesced ordering or unions changed: %#v", got)
}
}
func coalescedResolutionSnapshotsByScan(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 TestResolutionCoverageIndexMatchesOrderedScan(t *testing.T) {
paths := []Path{
Keys("group"),