diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index a309832..24d6a1d 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -269,7 +269,7 @@ unchanged until Stage 11 so collection is independently reviewable. Validate: `go test ./internal/engine -run 'Secret|Sensitivity|Inherited|Effective|Explain'`, engine race tests, and the everyday check. No new predicate order/count guarantee. -## Stage 10: Build and test indexed sensitivity triggers +## Stage 10: Build and test indexed sensitivity triggers — Complete Roadmap candidate 6, isolated algorithm. Add private `internal/engine/secret_propagation.go` and focused tests. diff --git a/internal/engine/secret_propagation.go b/internal/engine/secret_propagation.go new file mode 100644 index 0000000..b45a450 --- /dev/null +++ b/internal/engine/secret_propagation.go @@ -0,0 +1,69 @@ +package engine + +type secretTrigger struct { + target string + ordinary bool +} + +func closeSecretDependencies(paths map[string]Path, sensitive map[string]bool, dependencies, inherited []secretDependency) map[string]bool { + result := make(map[string]bool, len(sensitive)) + queue := make([]string, 0, len(sensitive)) + for pointer, classified := range sensitive { + if classified { + result[pointer] = true + queue = append(queue, pointer) + } + } + + var triggers pathPrefixIndex + triggerValues := make([]secretTrigger, 0, len(dependencies)+2*len(inherited)) + insert := func(dependency Path, target string, ordinary bool) { + if ordinary && !dependency.valid() { + return + } + id := len(triggerValues) + triggerValues = append(triggerValues, secretTrigger{target: target, ordinary: ordinary}) + triggers.insert(dependency.String(), id) + } + for _, dependency := range dependencies { + insert(dependency.dependency, dependency.target.String(), true) + } + for _, link := range inherited { + insert(link.dependency, link.target.String(), false) + insert(link.target, link.dependency.String(), false) + } + + fired := make([]bool, len(triggerValues)) + activate := func(id int) { + if fired[id] { + return + } + fired[id] = true + target := triggerValues[id].target + if !result[target] { + result[target] = true + queue = append(queue, target) + } + } + for head := 0; head < len(queue); head++ { + pointer := queue[head] + if classified, ok := paths[pointer]; ok && classified.valid() { + triggers.visitAncestors(classified.String(), func(id int) { + if triggerValues[id].ordinary { + activate(id) + } + }) + triggers.visitDescendants(classified.String(), func(id int) { + if triggerValues[id].ordinary { + activate(id) + } + }) + } + triggers.visitDescendants(pointer, func(id int) { + if !triggerValues[id].ordinary { + activate(id) + } + }) + } + return result +} diff --git a/internal/engine/secret_propagation_test.go b/internal/engine/secret_propagation_test.go new file mode 100644 index 0000000..a51415b --- /dev/null +++ b/internal/engine/secret_propagation_test.go @@ -0,0 +1,127 @@ +package engine + +import ( + "reflect" + "testing" +) + +func TestSecretDependencyClosureMatchesFixedPointScan(t *testing.T) { + tests := []struct { + name string + paths map[string]Path + seed map[string]bool + dependencies []secretDependency + inherited []secretDependency + }{ + { + name: "forward reverse chains and cycles", + paths: secretPaths(Keys("input"), Keys("derived"), Keys("output"), Keys("cycle")), + seed: secretSet(Keys("input")), + dependencies: []secretDependency{ + {dependency: Keys("input"), target: Keys("derived")}, + {dependency: Keys("derived"), target: Keys("output")}, + {dependency: Keys("output"), target: Keys("cycle")}, + {dependency: Keys("cycle"), target: Keys("derived")}, + {dependency: Keys("input"), target: Keys("derived")}, + }, + }, + { + name: "ordinary ancestor descendant and root relations", + paths: secretPaths(Path{}, Keys("branch", "leaf"), Keys("parent", "child")), + seed: secretSet(Keys("branch", "leaf"), Keys("parent")), + dependencies: []secretDependency{ + {dependency: Keys("branch"), target: Keys("from-descendant")}, + {dependency: Keys("parent", "child"), target: Keys("from-ancestor")}, + {dependency: Path{}, target: Keys("from-root")}, + }, + }, + { + name: "unindexed seed only activates inherited relations", + paths: secretPaths(Keys("ordinary"), Keys("copy")), + seed: map[string]bool{"untracked": true}, + dependencies: []secretDependency{ + {dependency: Keys("untracked"), target: Keys("ordinary")}, + }, + inherited: []secretDependency{ + {dependency: Keys("untracked"), target: Keys("copy")}, + }, + }, + { + name: "inheritance direction root escapes and numeric spellings", + paths: secretPaths(Path{}, Keys("a/b", "01"), Keys("source", "value")), + seed: secretSet(Path{}), + dependencies: []secretDependency{ + {dependency: Keys("a/b", "01"), target: Keys("ordinary-copy")}, + }, + inherited: []secretDependency{ + {dependency: Keys("a/b", "01"), target: Keys("destination", "01")}, + {dependency: Keys("source"), target: Keys("destination")}, + }, + }, + { + name: "inherited links do not trigger from classified descendants", + paths: secretPaths(Keys("parent", "secret", "nested")), + seed: secretSet(Keys("parent", "secret", "nested")), + inherited: []secretDependency{ + {dependency: Keys("parent", "secret"), target: Keys("child", "secret")}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + want := referenceSecretDependencyClosure(test.paths, test.seed, test.dependencies, test.inherited) + got := closeSecretDependencies(test.paths, test.seed, test.dependencies, test.inherited) + if !reflect.DeepEqual(got, want) { + t.Fatalf("closure = %#v, want %#v", got, want) + } + }) + } +} + +func referenceSecretDependencyClosure(paths map[string]Path, seed map[string]bool, dependencies, inherited []secretDependency) map[string]bool { + secret := make(map[string]bool, len(seed)) + for pointer, classified := range seed { + if classified { + secret[pointer] = true + } + } + for { + changed := false + for _, dependency := range dependencies { + if secretIntersectsPath(paths, secret, dependency.dependency) && !secret[dependency.target.String()] { + secret[dependency.target.String()] = true + changed = true + } + } + for _, link := range inherited { + if pathIsSecret(secret, link.dependency) && !secret[link.target.String()] { + secret[link.target.String()] = true + changed = true + } + if pathIsSecret(secret, link.target) && !secret[link.dependency.String()] { + secret[link.dependency.String()] = true + changed = true + } + } + if !changed { + return secret + } + } +} + +func secretPaths(paths ...Path) map[string]Path { + result := make(map[string]Path, len(paths)) + for _, path := range paths { + result[path.String()] = path + } + return result +} + +func secretSet(paths ...Path) map[string]bool { + result := make(map[string]bool, len(paths)) + for _, path := range paths { + result[path.String()] = true + } + return result +}