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
|
## 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.
|
**Scope:** pre-resolution explanation replay, including inheritance and CLI.
|
||||||
|
|
||||||
1. Extend the query-local replay selection context in `explain.go` to use the
|
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
|
return Explanation{}, false
|
||||||
}
|
}
|
||||||
selections := model.indexReplaySelections(selected)
|
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)}
|
explanation := Explanation{Path: path.clone(), Effective: valueSnapshotWithLookup(r.final, path, r.secrets, model.documentLookup)}
|
||||||
if info, ok := r.metadata.Lookup(path); ok {
|
if info, ok := r.metadata.Lookup(path); ok {
|
||||||
explanation.Origin = cloneOrigin(info.Origin)
|
explanation.Origin = cloneOrigin(info.Origin)
|
||||||
@@ -57,7 +62,7 @@ func (r *Inspection) Explain(path Path) (Explanation, bool) {
|
|||||||
working := document.Clone(model.baseline)
|
working := document.Clone(model.baseline)
|
||||||
if baseline, found := selections.byPayload[historyPayloadRef{phase: historyPhaseBaseline, id: 0}]; found && baseline.after {
|
if baseline, found := selections.byPayload[historyPayloadRef{phase: historyPhaseBaseline, id: 0}]; found && baseline.after {
|
||||||
explanation.History = append(explanation.History, ValueChange{
|
explanation.History = append(explanation.History, ValueChange{
|
||||||
After: valueSnapshot(working, path, r.secrets),
|
After: valueSnapshot(working, entryPath, r.secrets),
|
||||||
Origin: cloneOrigin(baseline.attribution.origin),
|
Origin: cloneOrigin(baseline.attribution.origin),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -67,13 +72,13 @@ func (r *Inspection) Explain(path Path) (Explanation, bool) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
application := payload.application
|
application := payload.application
|
||||||
before := valueSnapshot(working, path, r.secrets)
|
before := valueSnapshot(working, entryPath, r.secrets)
|
||||||
applyHistoryApplication(&working, application, replayIndex)
|
applyHistoryApplication(&working, application, replayIndex)
|
||||||
selected, relevant := selections.application(historyPayloadRef{phase: historyPhaseSources, id: payloadID})
|
selected, relevant := selections.application(historyPayloadRef{phase: historyPhaseSources, id: payloadID})
|
||||||
if !relevant {
|
if !relevant {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
after := valueSnapshot(working, path, r.secrets)
|
after := valueSnapshot(working, entryPath, r.secrets)
|
||||||
if !before.Present && !after.Present {
|
if !before.Present && !after.Present {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -83,20 +88,20 @@ func (r *Inspection) Explain(path Path) (Explanation, bool) {
|
|||||||
Origin: cloneOrigin(selected.attribution.origin),
|
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:
|
// Intermediate input replacements do not permanently suppress defaults:
|
||||||
// decoding applies them to the final input tree. Show that reconciliation
|
// decoding applies them to the final input tree. Show that reconciliation
|
||||||
// explicitly before typed CLI contributions and domain resolution.
|
// explicitly before typed CLI contributions and domain resolution.
|
||||||
if checkpoint := model.preCLICheckpoint; checkpoint != nil {
|
if checkpoint := model.preCLICheckpoint; checkpoint != nil {
|
||||||
selection, selected := selections.byPayload[historyPayloadRef{phase: historyPhasePreCLI, id: 0}]
|
selection, selected := selections.byPayload[historyPayloadRef{phase: historyPhasePreCLI, id: 0}]
|
||||||
replayed, _ := documentAtPath(working, path)
|
replayed, _ := documentAtPath(working, entryPath)
|
||||||
decoded, _ := documentAtPathWithLookup(checkpoint.root, path, model.documentLookup)
|
decoded, _ := documentAtPathWithLookup(checkpoint.root, entryPath, model.documentLookup)
|
||||||
if selected && !configurationValuesEqual(replayed, decoded) {
|
if selected && !configurationValuesEqual(replayed, decoded) {
|
||||||
origin, reasons := checkpointExplanationAttribution(selection)
|
origin, reasons := checkpointExplanationAttribution(selection)
|
||||||
explanation.History = append(explanation.History, ValueChange{
|
explanation.History = append(explanation.History, ValueChange{
|
||||||
Before: valueSnapshot(working, path, r.secrets),
|
Before: valueSnapshot(working, entryPath, r.secrets),
|
||||||
After: valueSnapshotWithLookup(checkpoint.root, path, r.secrets, model.documentLookup),
|
After: valueSnapshotWithLookup(checkpoint.root, entryPath, r.secrets, model.documentLookup),
|
||||||
Origin: origin,
|
Origin: origin,
|
||||||
Reasons: reasons,
|
Reasons: reasons,
|
||||||
})
|
})
|
||||||
@@ -109,13 +114,13 @@ func (r *Inspection) Explain(path Path) (Explanation, bool) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
applicationRef := historyPayloadRef{phase: historyPhaseCLI, id: payloadID}
|
applicationRef := historyPayloadRef{phase: historyPhaseCLI, id: payloadID}
|
||||||
before := valueSnapshot(working, path, r.secrets)
|
before := valueSnapshot(working, entryPath, r.secrets)
|
||||||
applyHistoryApplication(&working, payload.application, replayIndex)
|
applyHistoryApplication(&working, payload.application, replayIndex)
|
||||||
selected, relevant := selections.application(applicationRef)
|
selected, relevant := selections.application(applicationRef)
|
||||||
if !relevant {
|
if !relevant {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
after := valueSnapshot(working, path, r.secrets)
|
after := valueSnapshot(working, entryPath, r.secrets)
|
||||||
if !before.Present && !after.Present {
|
if !before.Present && !after.Present {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -127,13 +132,13 @@ func (r *Inspection) Explain(path Path) (Explanation, bool) {
|
|||||||
}
|
}
|
||||||
if checkpoint := model.postCLICheckpoint; checkpoint != nil {
|
if checkpoint := model.postCLICheckpoint; checkpoint != nil {
|
||||||
selection, selected := selections.byPayload[historyPayloadRef{phase: historyPhasePostCLI, id: 0}]
|
selection, selected := selections.byPayload[historyPayloadRef{phase: historyPhasePostCLI, id: 0}]
|
||||||
replayed, _ := documentAtPath(working, path)
|
replayed, _ := documentAtPath(working, entryPath)
|
||||||
effectiveInput, _ := documentAtPathWithLookup(checkpoint.root, path, model.documentLookup)
|
effectiveInput, _ := documentAtPathWithLookup(checkpoint.root, entryPath, model.documentLookup)
|
||||||
if selected && !configurationValuesEqual(replayed, effectiveInput) {
|
if selected && !configurationValuesEqual(replayed, effectiveInput) {
|
||||||
origin, reasons := checkpointExplanationAttribution(selection)
|
origin, reasons := checkpointExplanationAttribution(selection)
|
||||||
explanation.History = append(explanation.History, ValueChange{
|
explanation.History = append(explanation.History, ValueChange{
|
||||||
Before: valueSnapshot(working, path, r.secrets),
|
Before: valueSnapshot(working, entryPath, r.secrets),
|
||||||
After: valueSnapshotWithLookup(checkpoint.root, path, r.secrets, model.documentLookup),
|
After: valueSnapshotWithLookup(checkpoint.root, entryPath, r.secrets, model.documentLookup),
|
||||||
Origin: origin,
|
Origin: origin,
|
||||||
Reasons: reasons,
|
Reasons: reasons,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -303,6 +303,23 @@ func TestRegisteredResolutionHistoryFollowsMatchedSource(t *testing.T) {
|
|||||||
if !found {
|
if !found {
|
||||||
t.Errorf("missing source line %d in %#v", check.line, history)
|
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")) {
|
for _, change := range result.Metadata().History(Keys("items").Index(2).Key("value")) {
|
||||||
if change.Origin.Layer == LayerMain {
|
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