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
|
||||
|
||||
**Status:** Complete. Focused history and explanation checks passed for moves, removals, additions, and reused positions.
|
||||
|
||||
**Scope:** resolution replay groups, structural changes, and reused addresses.
|
||||
|
||||
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
|
||||
}
|
||||
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)
|
||||
after := valueSnapshot(working, path, r.secrets)
|
||||
if !before.Present && !after.Present {
|
||||
|
||||
@@ -2,6 +2,7 @@ package engine
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -390,3 +391,145 @@ func TestMovedRecordExplanationIncludesCLIValue(t *testing.T) {
|
||||
}
|
||||
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