From 34c3241dae56be5d8d1fe56c7cf427b0e9e67af8 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 2 Nov 2026 17:29:36 +0000 Subject: [PATCH] Report moved history at final record paths --- docs/roadmap/remediation.md | 8 ++- internal/engine/history_select.go | 9 ++-- .../engine/resolution_remediation_test.go | 54 +++++++++++++++++++ 3 files changed, 65 insertions(+), 6 deletions(-) diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index 7254d47..4fee4b8 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -74,7 +74,13 @@ enumeration or new public addressing mechanism. ## Finding 2 — Deep descendant history reports the old record's operation path -**Priority:** P2. **Status:** Open. +**Priority:** P2. **Status:** Complete. + +Completion: pre-resolution operation paths now use the matched record-prefix +translation, preserving ancestor scope and suffixes. Resolution records already +in final coordinates are left unchanged. A regression covers deep inherited +fields, record/container queries, original source coordinates, and overlapping +final-position records. Focused history checks and `./scripts/check.sh` passed. **Code:** [shared history selection](../../internal/engine/history_select.go), `selectHistory`, especially the reported-path translation at lines 116–120. diff --git a/internal/engine/history_select.go b/internal/engine/history_select.go index cdcd012..12d33b0 100644 --- a/internal/engine/history_select.go +++ b/internal/engine/history_select.go @@ -113,11 +113,10 @@ func (model *historyModel) selectHistory(path Path) []historySelection { } else if !historyObservationMatchesQuery(query, observation.path) { continue } - if query.String() != path.String() && pathContains(query, reportedPath) { - suffix := reportedPath.segments[len(query.segments):] - reportedPath = Path{segments: append(clonePathSegments(path.segments, len(suffix)), suffix...)} - } else if query.String() != path.String() && pathContains(reportedPath, query) && len(reportedPath.segments) >= len(query.segments)-1 { - reportedPath = path.clone() + if observation.phase != historyPhaseResolution && query.String() != path.String() { + if translated, matched := model.resolutionPayload.correspondence.finalPath(reportedPath); matched { + reportedPath = translated + } } selectionIndex, found := selectionByPayload[observation.payload] if !found { diff --git a/internal/engine/resolution_remediation_test.go b/internal/engine/resolution_remediation_test.go index 29c72e5..7e739bc 100644 --- a/internal/engine/resolution_remediation_test.go +++ b/internal/engine/resolution_remediation_test.go @@ -40,3 +40,57 @@ func TestMovedRemovedFieldHasConsistentHistory(t *testing.T) { } } } + +func TestMovedInheritedHistoryUsesFinalRecordPrefix(t *testing.T) { + type config struct { + Items []map[string]any `yaml:"items"` + } + field := Keys("items").Index(0).Key("nested").Key("more").Key("value") + 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") + r.RecordNormalization(field, "retain inherited field") + return nil + }, + }, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}}) + result, err := testLoad(Bytes("records", []byte("items:\n - id: parent\n nested: {more: {value: old}}\n - id: child\n extends: {id: parent}\n")), Options[config]{Definition: definition}) + if err != nil { + t.Fatal(err) + } + physical := result.Metadata().History(Keys("items")) + sourceID := result.Inspection.Sources()[0].ID + for _, query := range []Path{field, Keys("items").Index(0)} { + inherited, recorded := false, false + for _, change := range result.Metadata().History(query) { + if change.Origin.Operation == ChangeInheritance { + inherited = true + if !pathContains(Keys("items").Index(0), change.Path) { + t.Errorf("inheritance path for %s = %s", query.String(), change.Path.String()) + } + if query.String() == field.String() && change.Path.String() != "/items/0" { + t.Errorf("container operation replaced by leaf path: %s", change.Path.String()) + } + for _, location := range change.Origin.Locations { + if location.SourceID != sourceID || (query.String() == field.String() && location.Line != 3) { + t.Errorf("source coordinates changed: %#v", location) + } + } + } + if change.Origin.Layer == LayerResolution && len(change.Reasons) == 1 && change.Reasons[0] == "retain inherited field" { + recorded = true + if change.Path.String() != field.String() { + t.Errorf("final resolution path translated twice: %s", change.Path.String()) + } + } + } + if !inherited || !recorded { + t.Errorf("missing inherited or recorded evidence for %s", query.String()) + } + } + for _, change := range physical { + if change.Origin.Operation == ChangeInheritance && !pathContains(Keys("items").Index(1), change.Path) { + t.Errorf("collection history lost physical coordinates: %s", change.Path.String()) + } + } +}