diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index a009636..a309832 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -245,7 +245,7 @@ Validate: `go test ./internal/engine -run 'Resolution|Metadata|Provenance|Histor engine race tests, and the everyday check. Complexity is output-sensitive to affected events, not every delta times every field. -## Stage 9: Collect sensitivity directly from trees +## Stage 9: Collect sensitivity directly from trees — Complete Roadmap candidate 5. Owner: engine `secret.go`. Keep fixed-point propagation unchanged until Stage 11 so collection is independently reviewable. diff --git a/internal/engine/secret.go b/internal/engine/secret.go index b91795f..7490fa3 100644 --- a/internal/engine/secret.go +++ b/internal/engine/secret.go @@ -9,17 +9,10 @@ type secretDependency struct { func classifySecretPaths(schema *schemaDescriptor, predicate func(Path) bool, metadata Metadata, final *document.Node, applications []historyApplication, projections ...*document.Node) map[string]bool { paths := make(map[string]Path) - finalPaths := make(map[string]*document.Node) - indexDocument(final, Path{}, finalPaths) - for pointer := range finalPaths { - addSecretPath(paths, pointer) - } for _, application := range applications { - applicationPaths := make(map[string]*document.Node) - indexDocument(application.value, application.path, applicationPaths) - for pointer := range applicationPaths { - addSecretPath(paths, pointer) - } + walkDocument(application.value, application.path, func(path Path, _ *document.Node) { + addSecretPath(paths, path.String()) + }) } var dependencies []secretDependency for pointer, history := range metadata.history { @@ -65,16 +58,19 @@ func classifySecretPaths(schema *schemaDescriptor, predicate func(Path) bool, me } secret := make(map[string]bool) - for _, tree := range append(projections, final) { - projected := make(map[string]*document.Node) - indexDocument(tree, Path{}, projected) - for pointer, node := range projected { + classifyProjection := func(tree *document.Node) { + walkDocument(tree, Path{}, func(path Path, node *document.Node) { + pointer := path.String() addSecretPath(paths, pointer) if node.Sensitive { secret[pointer] = true } - } + }) } + for _, tree := range projections { + classifyProjection(tree) + } + classifyProjection(final) for pointer, path := range paths { if root, ok := schemaSecretRoot(schema, path); ok { secret[root.String()] = true diff --git a/internal/engine/secret_classification_test.go b/internal/engine/secret_classification_test.go new file mode 100644 index 0000000..6b2502f --- /dev/null +++ b/internal/engine/secret_classification_test.go @@ -0,0 +1,23 @@ +package engine + +import "testing" + +func TestSecretPredicateReceivesDetachedHistoricalPath(t *testing.T) { + path := Keys("records").Key("1") + metadata := Metadata{history: map[string][]Change{ + path.String(): {{Path: path}}, + }} + seen := false + classified := classifySecretPaths(nil, func(candidate Path) bool { + if candidate.String() != path.String() { + return false + } + seen = true + candidate.segments[0].key = "changed" + return true + }, metadata, nil, nil) + + if !seen || !classified[path.String()] || classified["/changed/1"] { + t.Fatalf("classification after predicate mutation = %#v, predicate called: %v", classified, seen) + } +}