From 3f389662e9d392471540d560d6df68d0badf01d0 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Tue, 27 Oct 2026 03:05:15 +0000 Subject: [PATCH] Keep explanation origins scoped to the query --- docs/roadmap/remediation.md | 8 ++++++- facade_container_origin_test.go | 41 +++++++++++++++++++++++++++++++++ internal/engine/explain.go | 8 ++++--- 3 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 facade_container_origin_test.go diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index dfee139..2547267 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -162,7 +162,13 @@ history regression checking source chronology and previous-origin links. ## 3. Container Explanations Take The Last Descendant Replacement's Origin -Priority: P2. Status: open. +Priority: P2. Status: complete. + +Completion: explanation uses the model's direct query-scoped contribution when +available and reserves operation fallback for paths without that contribution. +`TestOverlayExplanationUsesQueryScopedOrigin` failed before the fix and now +checks root, enclosing mapping, and leaf locations. The full ordinary suite and +`git diff --check` passed, including removed/replaced descendant coverage. Relevant code: `historySelectionForApplication` in [explain.go](../../internal/engine/explain.go), lines 183–207; and descendant diff --git a/facade_container_origin_test.go b/facade_container_origin_test.go new file mode 100644 index 0000000..72863bb --- /dev/null +++ b/facade_container_origin_test.go @@ -0,0 +1,41 @@ +package configkit_test + +import ( + "testing" + "testing/fstest" + + configkit "gitea.maximumdirect.net/eric/ConfigKit" +) + +func TestOverlayExplanationUsesQueryScopedOrigin(t *testing.T) { + t.Parallel() + filesystem := configkit.NewFilesystem("origin", fstest.MapFS{ + "main.yml": {Data: []byte("a: base\ngroup: {x: X, y: Y}\n")}, + "overlays/dev.yml": {Data: []byte("a: overlay\ngroup: {x: O}\n")}, + }) + result, err := configkit.Load[map[string]any](filesystem.File("main.yml"), configkit.Options[map[string]any]{Overlay: "dev"}) + if err != nil { + t.Fatal(err) + } + for _, test := range []struct { + path configkit.Path + line, column int + }{ + {configkit.Keys(), 1, 1}, + {configkit.Keys("group"), 2, 8}, + {configkit.Keys("group", "x"), 2, 12}, + } { + explanation, found := result.Explain(test.path) + if !found || len(explanation.History) == 0 { + t.Fatalf("missing explanation for %s", test.path) + } + origin := explanation.History[len(explanation.History)-1].Origin + if origin.Layer != configkit.LayerOverlay || len(origin.Locations) != 1 { + t.Fatalf("overlay origin for %s = %#v", test.path, origin) + } + location := origin.Locations[0] + if location.SourceID != "source-2" || location.Line != test.line || location.Column != test.column { + t.Fatalf("overlay location for %s = %#v; want line %d column %d", test.path, location, test.line, test.column) + } + } +} diff --git a/internal/engine/explain.go b/internal/engine/explain.go index 6aca451..e779944 100644 --- a/internal/engine/explain.go +++ b/internal/engine/explain.go @@ -181,11 +181,13 @@ func historySelectionForPayload(selections []historySelection, ref historyPayloa } func historySelectionForApplication(model *historyModel, selections []historySelection, phase historyPhase, applicationRef historyPayloadRef) (historySelection, bool) { + if direct, ok := historySelectionForPayload(selections, applicationRef); ok { + return direct, true + } + // Only removed/replaced paths lacking a direct contribution need an + // operation fallback. A descendant's location cannot describe its container. var selected historySelection found := false - if direct, ok := historySelectionForPayload(selections, applicationRef); ok { - selected, found = direct, true - } var payloads []historySourcePayload switch phase { case historyPhaseSources: