From c20ea4b9163d7bcc9e52dc812245c7726e55b558 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Fri, 9 Oct 2026 12:16:13 +0000 Subject: [PATCH] Index combined diff sensitivity --- docs/roadmap/implementation.md | 2 +- internal/engine/diff_sensitivity.go | 48 +++++++++++++ internal/engine/diff_sensitivity_test.go | 90 ++++++++++++++++++++++++ 3 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 internal/engine/diff_sensitivity.go create mode 100644 internal/engine/diff_sensitivity_test.go diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 4c42d62..ada2f60 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -177,7 +177,7 @@ no consumer-facing operation exposes them yet. ## Stage 3 — Combine sensitivity into a local lookup -Status: planned. Scope: classification lookup, independently of safe rendering. +Status: complete. Scope: classification lookup, independently of safe rendering. 1. Add `internal/engine/diff_sensitivity.go` and focused tests. Union true classifications from both inspections without mutating either map. diff --git a/internal/engine/diff_sensitivity.go b/internal/engine/diff_sensitivity.go new file mode 100644 index 0000000..802950f --- /dev/null +++ b/internal/engine/diff_sensitivity.go @@ -0,0 +1,48 @@ +package engine + +type diffSensitivity struct { + prefixes pathPrefixIndex + pointers []string +} + +func newDiffSensitivity(left, right map[string]bool) *diffSensitivity { + sensitivity := &diffSensitivity{} + seen := make(map[string]struct{}, len(left)+len(right)) + for _, classifications := range []map[string]bool{left, right} { + for pointer, classified := range classifications { + if !classified { + continue + } + if _, found := seen[pointer]; found { + continue + } + seen[pointer] = struct{}{} + id := len(sensitivity.pointers) + sensitivity.pointers = append(sensitivity.pointers, pointer) + sensitivity.prefixes.insert(pointer, id) + } + } + return sensitivity +} + +func (sensitivity *diffSensitivity) isSensitiveAtOrAbove(pointer string) bool { + if sensitivity == nil { + return false + } + found := false + sensitivity.prefixes.visitAncestors(pointer, func(int) { found = true }) + return found +} + +func (sensitivity *diffSensitivity) hasStrictDescendant(pointer string) bool { + if sensitivity == nil { + return false + } + found := false + sensitivity.prefixes.visitDescendants(pointer, func(id int) { + if sensitivity.pointers[id] != pointer { + found = true + } + }) + return found +} diff --git a/internal/engine/diff_sensitivity_test.go b/internal/engine/diff_sensitivity_test.go new file mode 100644 index 0000000..fe252b6 --- /dev/null +++ b/internal/engine/diff_sensitivity_test.go @@ -0,0 +1,90 @@ +package engine + +import ( + "maps" + "testing" +) + +func TestDiffSensitivityCombinesClassificationsWithoutMutation(t *testing.T) { + left := map[string]bool{ + "/a": true, + "/a/b": true, + "/historical/removed": true, + "/cross~1key": true, + "/literal~01slash": true, + "/items/01": true, + "/items/+1": false, + "/false-left": false, + } + right := map[string]bool{ + "/a/b": true, + "/right-only": true, + "/false-right": false, + } + leftBefore, rightBefore := maps.Clone(left), maps.Clone(right) + sensitivity := newDiffSensitivity(left, right) + + for _, test := range []struct { + pointer string + want bool + }{ + {pointer: "/a", want: true}, + {pointer: "/a/b", want: true}, + {pointer: "/a/b/c", want: true}, + {pointer: "/ab", want: false}, + {pointer: "/ab/c", want: false}, + {pointer: "/historical/removed", want: true}, + {pointer: "/historical/removed/child", want: true}, + {pointer: "/historical", want: false}, + {pointer: "/right-only", want: true}, + {pointer: "/false-left", want: false}, + {pointer: "/false-right", want: false}, + {pointer: "/cross~1key", want: true}, + {pointer: "/cross/key", want: false}, + {pointer: "/literal~01slash", want: true}, + {pointer: "/literal~1slash", want: false}, + {pointer: "/items/01", want: true}, + {pointer: "/items/1", want: false}, + {pointer: "/items/+1", want: false}, + } { + if got := sensitivity.isSensitiveAtOrAbove(test.pointer); got != test.want { + t.Errorf("isSensitiveAtOrAbove(%q) = %v, want %v", test.pointer, got, test.want) + } + } + + for _, test := range []struct { + pointer string + want bool + }{ + {pointer: "", want: true}, + {pointer: "/a", want: true}, + {pointer: "/a/b", want: false}, + {pointer: "/ab", want: false}, + {pointer: "/historical", want: true}, + {pointer: "/right-only", want: false}, + {pointer: "/cross~1key", want: false}, + } { + if got := sensitivity.hasStrictDescendant(test.pointer); got != test.want { + t.Errorf("hasStrictDescendant(%q) = %v, want %v", test.pointer, got, test.want) + } + } + + if !maps.Equal(left, leftBefore) || !maps.Equal(right, rightBefore) { + t.Fatal("classification maps were modified") + } +} + +func TestDiffSensitivityDistinguishesRootAndEmptyKey(t *testing.T) { + root := newDiffSensitivity(map[string]bool{"": true}, nil) + if !root.isSensitiveAtOrAbove("") || !root.isSensitiveAtOrAbove("/empty") || + root.hasStrictDescendant("") { + t.Fatal("root classification did not remain distinct from strict descendants") + } + + emptyKey := newDiffSensitivity(map[string]bool{"/": true}, nil) + if emptyKey.isSensitiveAtOrAbove("") || !emptyKey.isSensitiveAtOrAbove("/") || + emptyKey.isSensitiveAtOrAbove("/other") || !emptyKey.hasStrictDescendant("") || + emptyKey.hasStrictDescendant("/") { + t.Fatal("empty-key classification was treated as the root or an unrelated path") + } +}