Replay matched record input history at entry positions
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user