Explain matched resolution and removed records
This commit is contained in:
@@ -307,6 +307,8 @@ every source form across every case. Collection queries keep original list order
|
|||||||
|
|
||||||
## Stage 9 — Explain net resolution and preserve removed evidence
|
## Stage 9 — Explain net resolution and preserve removed evidence
|
||||||
|
|
||||||
|
**Status:** Complete. Focused history and explanation checks passed for moves, removals, additions, and reused positions.
|
||||||
|
|
||||||
**Scope:** resolution replay groups, structural changes, and reused addresses.
|
**Scope:** resolution replay groups, structural changes, and reused addresses.
|
||||||
|
|
||||||
1. For matched-record queries, read a group's before side using the entry path
|
1. For matched-record queries, read a group's before side using the entry path
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ func (r *Inspection) Explain(path Path) (Explanation, bool) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
covering := model.resolutionPayload.snapshots[group.coveringRecordID]
|
covering := model.resolutionPayload.snapshots[group.coveringRecordID]
|
||||||
before := valueSnapshot(working, path, r.secrets)
|
before := valueSnapshot(working, entryPath, r.secrets)
|
||||||
replaceHistoryPath(&working, group.change.Path, covering.after)
|
replaceHistoryPath(&working, group.change.Path, covering.after)
|
||||||
after := valueSnapshot(working, path, r.secrets)
|
after := valueSnapshot(working, path, r.secrets)
|
||||||
if !before.Present && !after.Present {
|
if !before.Present && !after.Present {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package engine
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -390,3 +391,145 @@ func TestMovedRecordExplanationIncludesCLIValue(t *testing.T) {
|
|||||||
}
|
}
|
||||||
t.Fatalf("CLI history = %#v", explanation.History)
|
t.Fatalf("CLI history = %#v", explanation.History)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRegisteredResolutionExplainsRemovalAndReusedPositions(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 = value.Items[1:]
|
||||||
|
r.RecordNormalization(Keys("items"), "remove first")
|
||||||
|
return nil
|
||||||
|
}}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
|
||||||
|
result, err := testLoad(Bytes("records", []byte("items:\n - id: reports\n value: report\n - id: invoices\n value: invoice\n")), Options[config]{Definition: definition})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
path := Keys("items").Index(0).Key("value")
|
||||||
|
explanation, ok := result.Inspection.Explain(path)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("survivor explanation missing")
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for _, change := range explanation.History {
|
||||||
|
if change.Origin.Layer == LayerResolution {
|
||||||
|
if change.Before.YAML != "invoice\n" || change.After.YAML != "invoice\n" {
|
||||||
|
t.Fatalf("survivor operation = %#v", change)
|
||||||
|
}
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
if change.Origin.Layer == LayerMain && change.After.YAML == "report\n" {
|
||||||
|
t.Fatalf("removed occupant leaked into survivor: %#v", change)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatal("missing collection operation")
|
||||||
|
}
|
||||||
|
container, ok := result.Inspection.Explain(Keys("items"))
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("collection explanation missing")
|
||||||
|
}
|
||||||
|
found = false
|
||||||
|
for _, change := range container.History {
|
||||||
|
if change.Origin.Layer == LayerResolution && strings.Contains(change.Before.YAML, "reports") && !strings.Contains(change.After.YAML, "reports") {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("missing removed collection evidence: %#v", container.History)
|
||||||
|
}
|
||||||
|
mainEvidence := false
|
||||||
|
for _, change := range result.Metadata().History(Keys("items")) {
|
||||||
|
if change.Origin.Layer == LayerMain && len(change.Origin.Locations) != 0 {
|
||||||
|
mainEvidence = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !mainEvidence {
|
||||||
|
t.Fatal("collection history lost source attribution")
|
||||||
|
}
|
||||||
|
if _, ok := result.Inspection.Explain(Keys("items").Index(1)); !ok {
|
||||||
|
t.Fatal("absent historical position missing")
|
||||||
|
}
|
||||||
|
if _, ok := result.Inspection.Explain(Keys("items").Index(8)); ok {
|
||||||
|
t.Fatal("unknown position reported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegisteredResolutionExplainsAddedAndChangedIdentities(t *testing.T) {
|
||||||
|
type item struct {
|
||||||
|
ID string `yaml:"id"`
|
||||||
|
Value string `yaml:"value"`
|
||||||
|
}
|
||||||
|
type config struct {
|
||||||
|
Items []item `yaml:"items"`
|
||||||
|
}
|
||||||
|
for _, replace := range []bool{false, true} {
|
||||||
|
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{Resolve: func(value *config, r *Resolution) error {
|
||||||
|
if replace {
|
||||||
|
value.Items[0] = item{ID: "new", Value: "added"}
|
||||||
|
} else {
|
||||||
|
value.Items = append([]item{{ID: "new", Value: "added"}}, value.Items...)
|
||||||
|
}
|
||||||
|
r.RecordNormalization(Keys("items"), "update collection")
|
||||||
|
return nil
|
||||||
|
}}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
|
||||||
|
result, err := testLoad(Bytes("records", []byte("items: [{id: old, value: original}]\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("added explanation missing")
|
||||||
|
}
|
||||||
|
for _, change := range explanation.History {
|
||||||
|
if change.Origin.Layer == LayerMain || (change.Origin.Layer == LayerResolution && change.Before.Present) {
|
||||||
|
t.Fatalf("added record inherited previous value: %#v", explanation.History)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !replace {
|
||||||
|
moved, ok := result.Inspection.Explain(Keys("items").Index(1).Key("value"))
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("shifted survivor missing")
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for _, change := range moved.History {
|
||||||
|
if change.Origin.Layer == LayerResolution && change.Before.YAML == "original\n" && change.After.YAML == "original\n" {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("shifted survivor = %#v", moved.History)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegisteredResolutionExplainsRemovedFieldOnMovedRecord(t *testing.T) {
|
||||||
|
type config struct {
|
||||||
|
Items []map[string]any `yaml:"items"`
|
||||||
|
}
|
||||||
|
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{Resolve: func(value *config, r *Resolution) error {
|
||||||
|
delete(value.Items[0], "extra")
|
||||||
|
value.Items[0], value.Items[1] = value.Items[1], value.Items[0]
|
||||||
|
r.RecordNormalization(Keys("items"), "remove and reorder")
|
||||||
|
return nil
|
||||||
|
}}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
|
||||||
|
result, err := testLoad(Bytes("records", []byte("items: [{id: a, extra: own}, {id: b, extra: other}]\n")), Options[config]{Definition: definition})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
explanation, ok := result.Inspection.Explain(Keys("items").Index(1).Key("extra"))
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("removed field explanation missing")
|
||||||
|
}
|
||||||
|
for _, change := range explanation.History {
|
||||||
|
if change.Origin.Layer == LayerResolution && change.Before.YAML == "own\n" && !change.After.Present {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatalf("removed field history = %#v", explanation.History)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user