From db2d8ed2176fcbf37067990c857edbe6e6bce26d Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Fri, 9 Oct 2026 03:30:01 +0000 Subject: [PATCH] Add private canonical path prefix index --- docs/roadmap/implementation.md | 2 +- internal/engine/path_prefix.go | 138 ++++++++++++++++++++++++++++ internal/engine/path_prefix_test.go | 81 ++++++++++++++++ 3 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 internal/engine/path_prefix.go create mode 100644 internal/engine/path_prefix_test.go diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 8d7f9dc..8d63297 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -177,7 +177,7 @@ document comparison, and diagnostics. Validate: `go test ./internal/engine -run 'Resolution|Override|Overlay|Diagnostic|CompareDocument|Provenance'` and the everyday check. Expected benefit is reduced formatting, not measured CPU. -## Stage 6: Add a private canonical-pointer prefix index +## Stage 6: Add a private canonical-pointer prefix index — Complete Scope: one helper plus tests, no caller migration yet. diff --git a/internal/engine/path_prefix.go b/internal/engine/path_prefix.go new file mode 100644 index 0000000..4513db8 --- /dev/null +++ b/internal/engine/path_prefix.go @@ -0,0 +1,138 @@ +package engine + +type pathPrefixIndex struct { + root pathPrefixNode +} + +type pathPrefixNode struct { + children map[string]*pathPrefixNode + ids []int + minimumID int + hasMinimum bool +} + +func (node *pathPrefixNode) includeID(id int) { + if !node.hasMinimum || id < node.minimumID { + node.minimumID = id + node.hasMinimum = true + } +} + +func (index *pathPrefixIndex) insert(pointer string, id int) { + node := &index.root + node.includeID(id) + visitCanonicalPointerTokens(pointer, func(token string) bool { + if node.children == nil { + node.children = make(map[string]*pathPrefixNode) + } + child := node.children[token] + if child == nil { + child = &pathPrefixNode{} + node.children[token] = child + } + node = child + node.includeID(id) + return true + }) + node.ids = append(node.ids, id) +} + +func (index *pathPrefixIndex) visitAncestors(pointer string, visit func(int)) { + node := &index.root + for _, id := range node.ids { + visit(id) + } + visitCanonicalPointerTokens(pointer, func(token string) bool { + child := node.children[token] + if child == nil { + return false + } + node = child + for _, id := range node.ids { + visit(id) + } + return true + }) +} + +func (index *pathPrefixIndex) visitDescendants(pointer string, visit func(int)) { + root := index.nodeAt(pointer) + if root == nil { + return + } + var walk func(*pathPrefixNode) + walk = func(node *pathPrefixNode) { + for _, id := range node.ids { + visit(id) + } + for _, child := range node.children { + walk(child) + } + } + walk(root) +} + +func (index *pathPrefixIndex) earliestIntersectingID(pointer string) (int, bool) { + bestID, found := 0, false + consider := func(id int) { + if !found || id < bestID { + bestID = id + found = true + } + } + node := &index.root + for _, id := range node.ids { + consider(id) + } + complete := visitCanonicalPointerTokens(pointer, func(token string) bool { + child := node.children[token] + if child == nil { + return false + } + node = child + for _, id := range node.ids { + consider(id) + } + return true + }) + if complete && node.hasMinimum { + consider(node.minimumID) + } + return bestID, found +} + +func (index *pathPrefixIndex) nodeAt(pointer string) *pathPrefixNode { + node := &index.root + complete := visitCanonicalPointerTokens(pointer, func(token string) bool { + child := node.children[token] + if child == nil { + return false + } + node = child + return true + }) + if !complete { + return nil + } + return node +} + +func visitCanonicalPointerTokens(pointer string, visit func(string) bool) bool { + if pointer == "" { + return true + } + start := 1 + for { + end := start + for end < len(pointer) && pointer[end] != '/' { + end++ + } + if !visit(pointer[start:end]) { + return false + } + if end == len(pointer) { + return true + } + start = end + 1 + } +} diff --git a/internal/engine/path_prefix_test.go b/internal/engine/path_prefix_test.go new file mode 100644 index 0000000..4a3f548 --- /dev/null +++ b/internal/engine/path_prefix_test.go @@ -0,0 +1,81 @@ +package engine + +import ( + "slices" + "strings" + "testing" +) + +func TestPathPrefixIndexMatchesPointerScans(t *testing.T) { + entries := []struct { + pointer string + id int + }{ + {pointer: "", id: 12}, + {pointer: "/", id: 9}, + {pointer: "/a", id: 6}, + {pointer: "/a", id: 2}, + {pointer: "/a/b", id: 4}, + {pointer: "/a/b/c", id: 0}, + {pointer: "/a/bc", id: 10}, + {pointer: "/a~1b", id: 3}, + {pointer: "/a~0b", id: 1}, + {pointer: "/a~01b", id: 5}, + {pointer: "/items/0", id: 8}, + {pointer: "/items/01", id: 7}, + {pointer: "/items/+1", id: 11}, + } + var index pathPrefixIndex + for _, entry := range entries { + index.insert(entry.pointer, entry.id) + } + queries := []string{ + "", "/", "/a", "/a/b", "/a/b/c", "/a/b/c/d", "/a/bc", "/a/bc/d", + "/a~1b", "/a~0b", "/a~01b", "/items", "/items/0", "/items/01", "/items/1", "/missing", + } + for _, query := range queries { + gotAncestors := make([]int, 0) + index.visitAncestors(query, func(id int) { gotAncestors = append(gotAncestors, id) }) + wantAncestors := make([]int, 0) + gotDescendants := make([]int, 0) + wantDescendants := make([]int, 0) + wantEarliest, foundEarliest := 0, false + for _, entry := range entries { + if pointerContainsByString(entry.pointer, query) { + wantAncestors = append(wantAncestors, entry.id) + } + if pointerContainsByString(query, entry.pointer) { + wantDescendants = append(wantDescendants, entry.id) + } + if pointerContainsByString(entry.pointer, query) || pointerContainsByString(query, entry.pointer) { + if !foundEarliest || entry.id < wantEarliest { + wantEarliest, foundEarliest = entry.id, true + } + } + } + index.visitDescendants(query, func(id int) { gotDescendants = append(gotDescendants, id) }) + if !slices.Equal(sortedPrefixIDs(gotAncestors), sortedPrefixIDs(wantAncestors)) { + t.Errorf("ancestors(%q) = %v, want %v", query, sortedPrefixIDs(gotAncestors), sortedPrefixIDs(wantAncestors)) + } + if !slices.Equal(sortedPrefixIDs(gotDescendants), sortedPrefixIDs(wantDescendants)) { + t.Errorf("descendants(%q) = %v, want %v", query, sortedPrefixIDs(gotDescendants), sortedPrefixIDs(wantDescendants)) + } + gotEarliest, gotFound := index.earliestIntersectingID(query) + if gotEarliest != wantEarliest || gotFound != foundEarliest { + t.Errorf("earliestIntersectingID(%q) = %d, %v, want %d, %v", query, gotEarliest, gotFound, wantEarliest, foundEarliest) + } + } +} + +func pointerContainsByString(container, descendant string) bool { + if container == "" { + return true + } + return descendant == container || strings.HasPrefix(descendant, container+"/") +} + +func sortedPrefixIDs(ids []int) []int { + result := slices.Clone(ids) + slices.Sort(result) + return result +}