Index combined diff sensitivity
This commit is contained in:
@@ -177,7 +177,7 @@ no consumer-facing operation exposes them yet.
|
|||||||
|
|
||||||
## Stage 3 — Combine sensitivity into a local lookup
|
## 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
|
1. Add `internal/engine/diff_sensitivity.go` and focused tests. Union true
|
||||||
classifications from both inspections without mutating either map.
|
classifications from both inspections without mutating either map.
|
||||||
|
|||||||
48
internal/engine/diff_sensitivity.go
Normal file
48
internal/engine/diff_sensitivity.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
90
internal/engine/diff_sensitivity_test.go
Normal file
90
internal/engine/diff_sensitivity_test.go
Normal file
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user