Replay matched record input history at entry positions

This commit is contained in:
2026-11-02 14:52:05 +00:00
parent 151b56e7ec
commit 0799e1d18f
3 changed files with 101 additions and 14 deletions

View File

@@ -280,6 +280,8 @@ replay and removed-position handling using this same selection owner.
## Stage 8 — Replay earlier explanation values at the entry coordinate
**Status:** Complete. Focused source, inheritance, CLI, history, and explanation checks passed.
**Scope:** pre-resolution explanation replay, including inheritance and CLI.
1. Extend the query-local replay selection context in `explain.go` to use the

View File

@@ -49,6 +49,11 @@ func (r *Inspection) Explain(path Path) (Explanation, bool) {
return Explanation{}, false
}
selections := model.indexReplaySelections(selected)
coordinate := model.queryCoordinates(path)
entryPath := coordinate.entry
if !coordinate.hasEntry {
entryPath = Path{invalid: true}
}
explanation := Explanation{Path: path.clone(), Effective: valueSnapshotWithLookup(r.final, path, r.secrets, model.documentLookup)}
if info, ok := r.metadata.Lookup(path); ok {
explanation.Origin = cloneOrigin(info.Origin)
@@ -57,7 +62,7 @@ func (r *Inspection) Explain(path Path) (Explanation, bool) {
working := document.Clone(model.baseline)
if baseline, found := selections.byPayload[historyPayloadRef{phase: historyPhaseBaseline, id: 0}]; found && baseline.after {
explanation.History = append(explanation.History, ValueChange{
After: valueSnapshot(working, path, r.secrets),
After: valueSnapshot(working, entryPath, r.secrets),
Origin: cloneOrigin(baseline.attribution.origin),
})
}
@@ -67,13 +72,13 @@ func (r *Inspection) Explain(path Path) (Explanation, bool) {
continue
}
application := payload.application
before := valueSnapshot(working, path, r.secrets)
before := valueSnapshot(working, entryPath, r.secrets)
applyHistoryApplication(&working, application, replayIndex)
selected, relevant := selections.application(historyPayloadRef{phase: historyPhaseSources, id: payloadID})
if !relevant {
continue
}
after := valueSnapshot(working, path, r.secrets)
after := valueSnapshot(working, entryPath, r.secrets)
if !before.Present && !after.Present {
continue
}
@@ -83,20 +88,20 @@ func (r *Inspection) Explain(path Path) (Explanation, bool) {
Origin: cloneOrigin(selected.attribution.origin),
})
}
appendInheritanceExplanation(&explanation, &working, path, model, selections, r.secrets)
appendInheritanceExplanation(&explanation, &working, entryPath, model, selections, r.secrets)
// Intermediate input replacements do not permanently suppress defaults:
// decoding applies them to the final input tree. Show that reconciliation
// explicitly before typed CLI contributions and domain resolution.
if checkpoint := model.preCLICheckpoint; checkpoint != nil {
selection, selected := selections.byPayload[historyPayloadRef{phase: historyPhasePreCLI, id: 0}]
replayed, _ := documentAtPath(working, path)
decoded, _ := documentAtPathWithLookup(checkpoint.root, path, model.documentLookup)
replayed, _ := documentAtPath(working, entryPath)
decoded, _ := documentAtPathWithLookup(checkpoint.root, entryPath, model.documentLookup)
if selected && !configurationValuesEqual(replayed, decoded) {
origin, reasons := checkpointExplanationAttribution(selection)
explanation.History = append(explanation.History, ValueChange{
Before: valueSnapshot(working, path, r.secrets),
After: valueSnapshotWithLookup(checkpoint.root, path, r.secrets, model.documentLookup),
Before: valueSnapshot(working, entryPath, r.secrets),
After: valueSnapshotWithLookup(checkpoint.root, entryPath, r.secrets, model.documentLookup),
Origin: origin,
Reasons: reasons,
})
@@ -109,13 +114,13 @@ func (r *Inspection) Explain(path Path) (Explanation, bool) {
continue
}
applicationRef := historyPayloadRef{phase: historyPhaseCLI, id: payloadID}
before := valueSnapshot(working, path, r.secrets)
before := valueSnapshot(working, entryPath, r.secrets)
applyHistoryApplication(&working, payload.application, replayIndex)
selected, relevant := selections.application(applicationRef)
if !relevant {
continue
}
after := valueSnapshot(working, path, r.secrets)
after := valueSnapshot(working, entryPath, r.secrets)
if !before.Present && !after.Present {
continue
}
@@ -127,13 +132,13 @@ func (r *Inspection) Explain(path Path) (Explanation, bool) {
}
if checkpoint := model.postCLICheckpoint; checkpoint != nil {
selection, selected := selections.byPayload[historyPayloadRef{phase: historyPhasePostCLI, id: 0}]
replayed, _ := documentAtPath(working, path)
effectiveInput, _ := documentAtPathWithLookup(checkpoint.root, path, model.documentLookup)
replayed, _ := documentAtPath(working, entryPath)
effectiveInput, _ := documentAtPathWithLookup(checkpoint.root, entryPath, model.documentLookup)
if selected && !configurationValuesEqual(replayed, effectiveInput) {
origin, reasons := checkpointExplanationAttribution(selection)
explanation.History = append(explanation.History, ValueChange{
Before: valueSnapshot(working, path, r.secrets),
After: valueSnapshotWithLookup(checkpoint.root, path, r.secrets, model.documentLookup),
Before: valueSnapshot(working, entryPath, r.secrets),
After: valueSnapshotWithLookup(checkpoint.root, entryPath, r.secrets, model.documentLookup),
Origin: origin,
Reasons: reasons,
})

View File

@@ -303,6 +303,23 @@ func TestRegisteredResolutionHistoryFollowsMatchedSource(t *testing.T) {
if !found {
t.Errorf("missing source line %d in %#v", check.line, history)
}
explanation, ok := result.Inspection.Explain(path)
if !ok {
t.Fatalf("missing explanation for %s", path.String())
}
want := "second\n"
if check.index == 1 {
want = "first\n"
}
foundValue := false
for _, change := range explanation.History {
if change.Origin.Layer == LayerMain && change.After.YAML == want {
foundValue = true
}
}
if !foundValue {
t.Errorf("missing matched input %q in %#v", want, explanation.History)
}
}
for _, change := range result.Metadata().History(Keys("items").Index(2).Key("value")) {
if change.Origin.Layer == LayerMain {
@@ -310,3 +327,66 @@ func TestRegisteredResolutionHistoryFollowsMatchedSource(t *testing.T) {
}
}
}
func TestMovedRecordExplanationIncludesInheritedValue(t *testing.T) {
type item struct {
ID string `yaml:"id"`
Value string `yaml:"value"`
}
type config struct {
Items []item `yaml:"items"`
}
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{Resolve: func(value *config, r *Resolution) error {
value.Items[0], value.Items[1] = value.Items[1], value.Items[0]
r.RecordNormalization(Keys("items"), "reorder")
return nil
}}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
result, err := testLoad(Bytes("records", []byte("items: [{id: parent, value: inherited}, {id: child, extends: {id: parent}}]\n")), Options[config]{Definition: definition})
if err != nil {
t.Fatal(err)
}
explanation, ok := result.Inspection.Explain(Keys("items").Index(0).Key("value"))
if !ok {
t.Fatal("missing explanation")
}
found := false
for _, change := range explanation.History {
if change.After.YAML == "inherited\n" {
found = true
}
}
if !found {
t.Fatalf("inherited history = %#v", explanation.History)
}
}
func TestMovedRecordExplanationIncludesCLIValue(t *testing.T) {
type item struct {
ID string `yaml:"id"`
Value string `yaml:"value"`
}
type config struct {
Items []item `yaml:"items"`
}
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{Resolve: func(value *config, r *Resolution) error {
value.Items[0], value.Items[1] = value.Items[1], value.Items[0]
r.RecordNormalization(Keys("items"), "reorder")
return nil
}}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
result, err := testLoad(NoSource(), Options[config]{Definition: definition, Overrides: []Override{{
Path: Keys("items"), Flag: "--items", Value: []any{map[string]any{"id": "a", "value": "first"}, map[string]any{"id": "b", "value": "second"}},
}}})
if err != nil {
t.Fatal(err)
}
explanation, ok := result.Inspection.Explain(Keys("items").Index(1).Key("value"))
if !ok {
t.Fatal("missing explanation")
}
for _, change := range explanation.History {
if change.Origin.Layer == LayerCLI && change.After.YAML == "first\n" {
return
}
}
t.Fatalf("CLI history = %#v", explanation.History)
}