From e878ce51ff5239fea1a9991d6b6f576a1f58992e Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 2 Nov 2026 17:36:55 +0000 Subject: [PATCH] Index collection prefixes for path correspondence --- docs/roadmap/remediation.md | 10 +- internal/engine/resolution_correspondence.go | 104 ++++++++++-------- .../engine/resolution_remediation_test.go | 42 +++++++ internal/engine/secret_propagation.go | 2 +- 4 files changed, 108 insertions(+), 50 deletions(-) diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index b19b6b2..d36d1c5 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -176,7 +176,15 @@ change was added to provide. ## Finding 4 — Correspondence lookup repeatedly serializes growing path prefixes -**Priority:** P2. **Status:** Open. +**Priority:** P2. **Status:** Complete. + +Completion: eligible collection prefixes now use the existing prefix index. +Translation formats the requested pointer once, retains concrete path-kind +checks, and copies the resulting path independently. No-eligible cases return +positionally without formatting prefixes; no-registration loads skip matching +construction entirely. Cloning rebuilds an independently owned index. Existing +matching/history/ownership coverage and a numeric-key/escaped-key regression +passed, along with `./scripts/check.sh`. **Code:** [correspondence translations](../../internal/engine/resolution_correspondence.go), `entryPath` at lines 96–125 and `finalPath` at lines 129–156; consumers include diff --git a/internal/engine/resolution_correspondence.go b/internal/engine/resolution_correspondence.go index fd2dc79..f343084 100644 --- a/internal/engine/resolution_correspondence.go +++ b/internal/engine/resolution_correspondence.go @@ -13,28 +13,45 @@ type collectionCorrespondence struct { } type resolutionCorrespondence struct { - collections map[string]collectionCorrespondence + collections map[string]collectionCorrespondence + prefix pathPrefixIndex + indexedCollections []string } func (c resolutionCorrespondence) clone() resolutionCorrespondence { - result := resolutionCorrespondence{collections: make(map[string]collectionCorrespondence, len(c.collections))} - for key, collection := range c.collections { + var result resolutionCorrespondence + for _, collection := range c.collections { collection.path = collection.path.clone() collection.entryToFinal = append([]int(nil), collection.entryToFinal...) collection.finalToEntry = append([]int(nil), collection.finalToEntry...) - result.collections[key] = collection + result.indexCollection(collection) } return result } +func (c *resolutionCorrespondence) indexCollection(collection collectionCorrespondence) { + if c.collections == nil { + c.collections = make(map[string]collectionCorrespondence) + } + pointer := collection.path.String() + c.collections[pointer] = collection + if collection.eligible { + c.prefix.insert(pointer, len(c.indexedCollections)) + c.indexedCollections = append(c.indexedCollections, pointer) + } +} + func buildResolutionCorrespondence(entry, final *document.Node, policy sequencePolicy) resolutionCorrespondence { - result := resolutionCorrespondence{collections: make(map[string]collectionCorrespondence)} + var result resolutionCorrespondence + if policy.selectorTrie == nil { + return result + } var indexes mappingIndex var visit func(*document.Node, *document.Node, Path) visit = func(before, after *document.Node, path Path) { if fields, registered := policy.identityFieldsAt(path); registered && ((before != nil && before.Kind == document.Sequence) || (after != nil && after.Kind == document.Sequence)) { - result.collections[path.String()] = matchResolutionCollection(before, after, path, fields) + result.indexCollection(matchResolutionCollection(before, after, path, fields)) return } if before != nil && before.Kind == document.Mapping { @@ -94,64 +111,55 @@ func matchResolutionCollection(before, after *document.Node, path Path, fields [ // entryPath translates a surviving final record prefix; additions have no entry. // Paths outside eligible collections and wholly absent final positions stay positional. func (c resolutionCorrespondence) entryPath(path Path) (Path, bool) { - if !path.valid() { - return Path{}, false - } - for depth := 0; depth < len(path.segments); depth++ { - prefix := Path{segments: path.segments[:depth]} - collection, found := c.collections[prefix.String()] - if !found || !collection.eligible { - continue - } - compatible := true - for i, segment := range collection.path.segments { - if segment.kind == pathSegmentKey && path.segments[i].kind == pathSegmentIndex { - compatible = false - break - } - } - index, ok := schemaIndex(path.segments[depth]) - if !compatible || !ok || index >= len(collection.finalToEntry) { - continue - } - previous := collection.finalToEntry[index] - if previous < 0 { - return Path{}, false - } - translated := path.clone() - translated.segments[depth] = pathSegment{kind: pathSegmentIndex, index: previous} - return translated, true - } - return path.clone(), true + return c.translate(path, false) } // finalPath translates historical record descendants without matching removals. func (c resolutionCorrespondence) finalPath(path Path) (Path, bool) { + return c.translate(path, true) +} + +func (c resolutionCorrespondence) translate(path Path, toFinal bool) (Path, bool) { if !path.valid() { return Path{}, false } - for depth := 0; depth < len(path.segments); depth++ { - prefix := Path{segments: path.segments[:depth]} - collection, found := c.collections[prefix.String()] - if !found || !collection.eligible { - continue + translated := path.clone() + if len(c.indexedCollections) == 0 { + return translated, true + } + matched, handled := true, false + c.prefix.visitAncestors(path.String(), func(id int) { + if handled { + return + } + collection := c.collections[c.indexedCollections[id]] + depth := len(collection.path.segments) + if depth >= len(path.segments) { + return } for i, segment := range collection.path.segments { if segment.kind == pathSegmentKey && path.segments[i].kind == pathSegmentIndex { - return path.clone(), true + return } } index, ok := schemaIndex(path.segments[depth]) - if !ok || index >= len(collection.entryToFinal) { - continue + indices := collection.finalToEntry + if toFinal { + indices = collection.entryToFinal } - current := collection.entryToFinal[index] + if !ok || index >= len(indices) { + return + } + handled = true + current := indices[index] if current < 0 { - return Path{}, false + matched = false + return } - translated := path.clone() translated.segments[depth] = pathSegment{kind: pathSegmentIndex, index: current} - return translated, true + }) + if !matched { + return Path{}, false } - return path.clone(), true + return translated, true } diff --git a/internal/engine/resolution_remediation_test.go b/internal/engine/resolution_remediation_test.go index 2fcec16..78377b4 100644 --- a/internal/engine/resolution_remediation_test.go +++ b/internal/engine/resolution_remediation_test.go @@ -143,3 +143,45 @@ func TestCyclicMatchingProtectsRemovedAndDerivedValues(t *testing.T) { t.Fatal("effective rendering leaked") } } + +func TestCorrespondenceIndexPreservesPathShapes(t *testing.T) { + destination := Keys("groups", "0", "a/b~") + policy, diagnostics := compileSequenceIdentityPolicy([]sequenceIdentityRegistration{{ + selector: sequenceIdentitySelector{ + literalSequenceSelector("groups"), literalSequenceSelector("0"), literalSequenceSelector("a/b~"), + }, + identityFields: []string{"id"}, + }}) + if len(diagnostics) != 0 { + t.Fatal(diagnostics) + } + before := parseCompositionDocument(t, "before", "groups: {'0': {'a/b~': [{id: a}, {id: b}]}}") + after := parseCompositionDocument(t, "after", "groups: {'0': {'a/b~': [{id: b}, {id: a}, {id: new}]}}") + matching := buildResolutionCorrespondence(before, after, policy) + parsed, err := ParsePath("/groups/0/a~1b~0/0/field") + if err != nil { + t.Fatal(err) + } + for _, path := range []Path{destination.Index(0).Key("field"), parsed} { + entry, found := matching.entryPath(path) + if !found || entry.String() != destination.Index(1).Key("field").String() { + t.Fatalf("entry = %s, %t", entry.String(), found) + } + final, found := matching.finalPath(entry) + if !found || final.String() != path.String() { + t.Fatalf("round trip = %s, %t", final.String(), found) + } + } + for _, path := range []Path{ + Keys("groups").Index(0).Key("a/b~").Index(0), + destination.Key("0"), destination.Index(8), destination, + } { + entry, found := matching.entryPath(path) + if !found || entry.String() != path.String() { + t.Fatalf("positional path %s became %s, %t", path.String(), entry.String(), found) + } + } + if _, found := matching.entryPath(destination.Index(2)); found { + t.Fatal("new identity acquired entry evidence") + } +} diff --git a/internal/engine/secret_propagation.go b/internal/engine/secret_propagation.go index 21f99e8..30cb9f4 100644 --- a/internal/engine/secret_propagation.go +++ b/internal/engine/secret_propagation.go @@ -43,7 +43,7 @@ func closeSecretDependenciesWithCorrespondence(paths map[string]Path, sensitive } var projected map[string]bool - if len(correspondence.collections) != 0 { + if len(correspondence.indexedCollections) != 0 { projected = make(map[string]bool, len(paths)) } project := func(path Path) {